@conduction/nextcloud-vue 2.67.0 → 2.68.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/esm/components/CnContextMenu/CnContextMenu.vue2.js +37 -26
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue3.js +3 -3
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +87 -18
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +6 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +58 -48
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/defaultActions.js +6 -7
- package/dist/esm/components/CnIndexPage/defaultActions.js.map +1 -1
- package/dist/esm/components/CnRowActions/CnRowActions.vue2.js +33 -34
- package/dist/esm/components/CnRowActions/CnRowActions.vue2.js.map +1 -1
- package/dist/esm/components/CnRowActions/CnRowActions.vue3.js +3 -3
- package/dist/esm/components/CnRowActions/CnRowActions.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +8 -2
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +3 -2
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
- package/dist/esm/composables/useContextMenu.js +4 -2
- package/dist/esm/composables/useContextMenu.js.map +1 -1
- package/dist/esm/utils/resolveRowActions.js +130 -0
- package/dist/esm/utils/resolveRowActions.js.map +1 -0
- package/dist/esm/utils/rowActionItem.js +104 -0
- package/dist/esm/utils/rowActionItem.js.map +1 -0
- package/dist/esm/utils/timelineEvents.js +44 -1
- package/dist/esm/utils/timelineEvents.js.map +1 -1
- package/dist/esm/utils/validateManifest.js +59 -3
- package/dist/esm/utils/validateManifest.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +575 -151
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/package.json +1 -1
- package/scripts/.jsdoc-baselines.json +2 -2
- package/src/components/CnContextMenu/CnContextMenu.vue +40 -29
- package/src/components/CnFormDialog/CnFormDialog.vue +92 -18
- package/src/components/CnIndexPage/CnIndexPage.md +21 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +61 -51
- package/src/components/CnIndexPage/defaultActions.js +6 -7
- package/src/components/CnRowActions/CnRowActions.vue +36 -37
- package/src/components/CnSetupWizard/CnSetupWizard.vue +8 -2
- package/src/components/CnTimelineWidget/CnTimelineWidget.vue +3 -2
- package/src/composables/useContextMenu.js +4 -2
- package/src/schemas/app-manifest-v2.schema.json +46 -8
- package/src/utils/resolveRowActions.js +127 -0
- package/src/utils/rowActionItem.js +100 -0
- package/src/utils/timelineEvents.js +43 -0
- package/src/utils/validateManifest.js +60 -3
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -2,6 +2,7 @@ import { NcActions, NcActionButton, NcActionLink } from '@nextcloud/vue';
|
|
|
2
2
|
import { CTX_MENU_DATA_ATTR, CTX_MENU_POPPER_ATTR } from '../../composables/useContextMenu.js';
|
|
3
3
|
import { resolveItemActionLink, followItemActionLink } from '../../utils/actionLink.js';
|
|
4
4
|
import { isModifiedClick } from '../../utils/linkNavigation.js';
|
|
5
|
+
import { isRowActionVisible, slugifyActionLabel, rowActionTestId, rowActionKey, rowActionPayload } from '../../utils/rowActionItem.js';
|
|
5
6
|
import __sfc_main from '../CnIcon/CnIcon.vue.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -16,12 +17,13 @@ import __sfc_main from '../CnIcon/CnIcon.vue.js';
|
|
|
16
17
|
* cursor positioning). The composable handles the DOM attributes; this component
|
|
17
18
|
* handles the NcActions template boilerplate.
|
|
18
19
|
*
|
|
19
|
-
* Dynamic actions (CnIndexPage pattern)
|
|
20
|
+
* Dynamic actions (CnIndexPage pattern).
|
|
21
|
+
* CnIndexPage passes the same per-row list its row actions menu renders, so the two menus always match.
|
|
20
22
|
* ```vue
|
|
21
23
|
* <CnContextMenu
|
|
22
24
|
* v-model:open="contextMenuOpen"
|
|
23
|
-
* :actions="
|
|
24
|
-
* :target-item="
|
|
25
|
+
* :actions="rowActionsFor(contextMenuShownRow)"
|
|
26
|
+
* :target-item="contextMenuShownRow"
|
|
25
27
|
* @action="$emit('action', $event)"
|
|
26
28
|
* @close="closeContextMenu" />
|
|
27
29
|
* ```
|
|
@@ -68,13 +70,15 @@ var script = {
|
|
|
68
70
|
* `visible` (boolean | (targetItem) => boolean) hides the entry when falsy
|
|
69
71
|
* (default: shown). `title` (string | (targetItem) => string) renders as
|
|
70
72
|
* a native tooltip — useful for explaining why an entry is disabled.
|
|
73
|
+
* A locally decidable `visibleWhen` that is false for the target hides the entry too, as in CnRowActions.
|
|
74
|
+
* `id` is passed in the `action` payload; `builtin` marks a CnIndexPage built-in, whose testid is `cn-action-item-<id>` in every locale.
|
|
71
75
|
* `href` (URL) or `to` (vue-router location), each a value or a
|
|
72
76
|
* `(targetItem) => …` function, render the entry as a real link, with
|
|
73
77
|
* `linkTarget` as its `target`; a link emits `action` but does not call
|
|
74
78
|
* `handler`. When the entire array is empty (or all entries are filtered
|
|
75
79
|
* out), only the default slot content is rendered.
|
|
76
80
|
*
|
|
77
|
-
* @type {Array<{label: string, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}
|
|
81
|
+
* @type {Array<{label: string, id: string, builtin: boolean, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), visibleWhen: object, title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}
|
|
78
82
|
*/
|
|
79
83
|
actions: {
|
|
80
84
|
type: Array,
|
|
@@ -116,21 +120,13 @@ var script = {
|
|
|
116
120
|
|
|
117
121
|
computed: {
|
|
118
122
|
/**
|
|
119
|
-
* Filter actions by their `visible`
|
|
120
|
-
*
|
|
123
|
+
* Filter actions by their `visibleWhen` and `visible` gates, with the same rule CnRowActions uses.
|
|
124
|
+
* Entries without either always show.
|
|
121
125
|
*
|
|
122
126
|
* @return {Array} Visible actions for the current targetItem.
|
|
123
127
|
*/
|
|
124
128
|
visibleActions() {
|
|
125
|
-
return this.actions.filter((action) =>
|
|
126
|
-
if (action.visible === undefined) {
|
|
127
|
-
return true
|
|
128
|
-
}
|
|
129
|
-
if (typeof action.visible === 'function') {
|
|
130
|
-
return !!action.visible(this.targetItem)
|
|
131
|
-
}
|
|
132
|
-
return !!action.visible
|
|
133
|
-
})
|
|
129
|
+
return this.actions.filter((action) => isRowActionVisible(action, this.targetItem))
|
|
134
130
|
},
|
|
135
131
|
|
|
136
132
|
/**
|
|
@@ -373,18 +369,33 @@ var script = {
|
|
|
373
369
|
|
|
374
370
|
/**
|
|
375
371
|
* Slugify an action label for use in stable `data-testid` selectors.
|
|
376
|
-
* Lowercase, kebab-case, strip non-alphanumeric.
|
|
377
|
-
* `:data-testid` binding on NcActionButton — does not affect runtime
|
|
378
|
-
* behaviour or rendered text.
|
|
372
|
+
* Lowercase, kebab-case, strip non-alphanumeric.
|
|
379
373
|
*
|
|
380
374
|
* @param {string} label The action's display label.
|
|
381
375
|
* @return {string} kebab-case slug suitable for a testid suffix.
|
|
382
376
|
*/
|
|
383
377
|
slugifyLabel(label) {
|
|
384
|
-
return
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
378
|
+
return slugifyActionLabel(label)
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The entry's `data-testid`: `cn-action-item-<id>` for a built-in, the label slug otherwise.
|
|
383
|
+
*
|
|
384
|
+
* @param {object} action The action descriptor.
|
|
385
|
+
* @return {string} The testid.
|
|
386
|
+
*/
|
|
387
|
+
actionTestId(action) {
|
|
388
|
+
return rowActionTestId(action)
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The entry's render key: `builtin:<id>` for a built-in, the label otherwise.
|
|
393
|
+
*
|
|
394
|
+
* @param {object} action The action descriptor.
|
|
395
|
+
* @return {string} The key.
|
|
396
|
+
*/
|
|
397
|
+
actionKey(action) {
|
|
398
|
+
return rowActionKey(action)
|
|
388
399
|
},
|
|
389
400
|
|
|
390
401
|
onAction(action) {
|
|
@@ -392,10 +403,10 @@ var script = {
|
|
|
392
403
|
action.handler(this.targetItem);
|
|
393
404
|
}
|
|
394
405
|
/**
|
|
395
|
-
* @event action User picked an entry from the menu. The action's own `handler(targetItem)` (when present) ran synchronously before this event fires; the event lets parents observe / log the choice.
|
|
396
|
-
* @type {{ action: string, row: object|null }}
|
|
406
|
+
* @event action User picked an entry from the menu. The action's own `handler(targetItem)` (when present) ran synchronously before this event fires; the event lets parents observe / log the choice. Payload: `action` (the label), the target as `row`, the action's `id` when it has one, and `builtin: true` for a CnIndexPage built-in.
|
|
407
|
+
* @type {{ action: string, row: object|null, id?: string, builtin?: boolean }}
|
|
397
408
|
*/
|
|
398
|
-
this.$emit('action',
|
|
409
|
+
this.$emit('action', rowActionPayload(action, this.targetItem));
|
|
399
410
|
},
|
|
400
411
|
|
|
401
412
|
/**
|
|
@@ -413,7 +424,7 @@ var script = {
|
|
|
413
424
|
return
|
|
414
425
|
}
|
|
415
426
|
followItemActionLink(event, link, this.$router);
|
|
416
|
-
this.$emit('action',
|
|
427
|
+
this.$emit('action', rowActionPayload(action, this.targetItem));
|
|
417
428
|
},
|
|
418
429
|
|
|
419
430
|
onClose() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnContextMenu.vue2.js","sources":["../../../../src/components/CnContextMenu/CnContextMenu.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-context-menu-root\">\n\t\t<NcActions\n\t\t\tv-if=\"!activePanel\"\n\t\t\tref=\"actions\"\n\t\t\tv-model:open=\"internalOpen\"\n\t\t\t:manualOpen=\"true\"\n\t\t\t:forceMenu=\"true\"\n\t\t\tclass=\"cn-context-menu\"\n\t\t\tcontainer=\"body\"\n\t\t\tdata-testid=\"cn-context-menu\"\n\t\t\t@close=\"onClose\"\n\t\t\t@closed=\"onClosed\">\n\t\t\t<!-- Dynamic actions from array prop -->\n\t\t\t<template v-for=\"{ action, link } in renderedActions\" :key=\"action.label\">\n\t\t\t\t<!-- A navigate-only action is a real link, so it can be middle-clicked, opened in a new tab or copied. -->\n\t\t\t\t<NcActionLink\n\t\t\t\t\tv-if=\"link\"\n\t\t\t\t\t:href=\"link.href\"\n\t\t\t\t\t:target=\"link.target\"\n\t\t\t\t\t:title=\"resolveTitle(action)\"\n\t\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t\t:data-testid=\"`cn-action-item-${slugifyLabel(action.label)}`\"\n\t\t\t\t\tcloseAfterClick\n\t\t\t\t\t@click=\"onLinkAction(action, link, $event)\">\n\t\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ action.label }}\n\t\t\t\t</NcActionLink>\n\t\t\t\t<NcActionButton\n\t\t\t\t\tv-else\n\t\t\t\t\t:title=\"resolveTitle(action)\"\n\t\t\t\t\t:disabled=\"resolveDisabled(action)\"\n\t\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t\t:data-testid=\"`cn-action-item-${slugifyLabel(action.label)}`\"\n\t\t\t\t\tcloseAfterClick\n\t\t\t\t\t@click=\"onAction(action)\">\n\t\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ action.label }}\n\t\t\t\t</NcActionButton>\n\t\t\t</template>\n\n\t\t\t<!--\n\t\t\t@slot default\n\t\t\t@description Custom NcActionButton-family content rendered inside the\n\t\t\tNcActions menu. Use this for hardcoded buttons (Keepiq pattern) when\n\t\t\tthe `actions` prop is empty.\n\t\t-->\n\t\t\t<slot />\n\t\t</NcActions>\n\n\t\t<!-- CUSTOM PANEL — bypass NcActions, render arbitrary slot content\n\t\t anchored at the cursor. The `#panel:<name>` slot may contain any\n\t\t markup (grids, inputs, custom components) without the NcActions\n\t\t child-allowlist filter. -->\n\t\t<template v-if=\"activePanel && internalOpen\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-context-menu__backdrop\"\n\t\t\t\t@click=\"onClose\"\n\t\t\t\t@contextmenu.prevent=\"onClose\" />\n\t\t\t<div\n\t\t\t\tref=\"panel\"\n\t\t\t\tclass=\"cn-context-menu__panel\"\n\t\t\t\trole=\"menu\"\n\t\t\t\ttabindex=\"-1\"\n\t\t\t\tdata-testid=\"cn-context-menu-panel\"\n\t\t\t\t:data-panel=\"activePanel\"\n\t\t\t\t@keydown.esc.stop=\"onClose\"\n\t\t\t\t@click.stop>\n\t\t\t\t<!-- @slot panel:<name> Free-form custom panel content shown when\n\t\t\t\t `activePanel === '<name>'`. Bypasses the NcActions child\n\t\t\t\t allowlist — put any markup here (grids, inputs, custom\n\t\t\t\t components, etc.). The slot name is dynamic: define one\n\t\t\t\t `#panel:<name>` per panel you want to support. -->\n\t\t\t\t<!-- @binding {Function} back Clear `activePanel`, returning to the default action list. -->\n\t\t\t\t<!-- @binding {Function} close Close the entire menu, equivalent to clicking outside. -->\n\t\t\t\t<!-- @binding {*} targetItem The right-clicked item, forwarded from the `targetItem` prop. -->\n\t\t\t\t<slot\n\t\t\t\t\t:name=\"`panel:${activePanel}`\"\n\t\t\t\t\t:back=\"back\"\n\t\t\t\t\t:close=\"onClose\"\n\t\t\t\t\t:targetItem=\"targetItem\" />\n\t\t\t</div>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { NcActionButton, NcActionLink, NcActions } from '@nextcloud/vue'\nimport { CTX_MENU_DATA_ATTR, CTX_MENU_POPPER_ATTR } from '../../composables/useContextMenu.js'\nimport { followItemActionLink, resolveItemActionLink } from '../../utils/actionLink.js'\nimport { isModifiedClick } from '../../utils/linkNavigation.js'\nimport { CnIcon } from '../CnIcon/index.js'\n\n/**\n * CnContextMenu — Right-click context menu wrapper around NcActions.\n *\n * Provides a pre-configured NcActions instance that positions itself at the\n * cursor via the `useContextMenu` composable's CSS custom properties. Accepts\n * an `actions` array for the common dynamic case (like CnRowActions), and a\n * default slot for hardcoded NcActionButton content.\n *\n * Pair with `useContextMenu()` for state management (open/close, target item,\n * cursor positioning). The composable handles the DOM attributes; this component\n * handles the NcActions template boilerplate.\n *\n * Dynamic actions (CnIndexPage pattern)\n * ```vue\n * <CnContextMenu\n * v-model:open=\"contextMenuOpen\"\n * :actions=\"mergedActions\"\n * :target-item=\"contextMenuRow\"\n * @action=\"$emit('action', $event)\"\n * @close=\"closeContextMenu\" />\n * ```\n *\n * Custom buttons via slot (Keepiq pattern)\n * ```vue\n * <CnContextMenu\n * v-model:open=\"contextMenuOpen\"\n * @close=\"closeContextMenu\">\n * <NcActionButton close-after-click @click=\"onRename\">\n * <template #icon><PencilIcon :size=\"20\" /></template>\n * Rename\n * </NcActionButton>\n * </CnContextMenu>\n * ```\n */\nexport default {\n\tname: 'CnContextMenu',\n\n\tcomponents: {\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tNcActionLink,\n\t\tCnIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Whether the context menu is open. Bind with `v-model:open` — the `.sync`\n\t\t * modifier it replaces was removed in Vue 3 and binds nothing.\n\t\t * Bind to `useContextMenu().isOpen`.\n\t\t */\n\t\topen: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Action definitions rendered as NcActionButton items.\n\t\t * Same format as CnRowActions: `{ label, icon?, handler?, disabled?, visible?, title?, destructive? }`.\n\t\t * `icon` accepts either a component (rendered directly) or a string —\n\t\t * a string is treated as an MDI name and rendered via `CnIcon` (e.g.\n\t\t * `\"Eye\"`), which lets manifest (JSON) actions declare icons by name.\n\t\t * `visible` (boolean | (targetItem) => boolean) hides the entry when falsy\n\t\t * (default: shown). `title` (string | (targetItem) => string) renders as\n\t\t * a native tooltip — useful for explaining why an entry is disabled.\n\t\t * `href` (URL) or `to` (vue-router location), each a value or a\n\t\t * `(targetItem) => …` function, render the entry as a real link, with\n\t\t * `linkTarget` as its `target`; a link emits `action` but does not call\n\t\t * `handler`. When the entire array is empty (or all entries are filtered\n\t\t * out), only the default slot content is rendered.\n\t\t *\n\t\t * @type {Array<{label: string, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}\n\t\t */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The right-clicked item (row, folder, etc.). Passed to action `handler`\n\t\t * and `disabled` callbacks, and included in the `action` event payload.\n\t\t * Bind to `useContextMenu().targetItem`.\n\t\t */\n\t\ttargetItem: {\n\t\t\ttype: [Object, String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Name of the currently-active custom panel. When set (and the menu is\n\t\t * open), the default NcActions list is replaced by the matching\n\t\t * `#panel:<name>` slot, rendered free-form (no NcActions child filter).\n\t\t * Bind with `v-model:active-panel` — the component emits `update:activePanel(null)` when\n\t\t * the panel's `back()` binding is invoked or the menu closes.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tactivePanel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['action', 'close', 'closed', 'update:activePanel', 'update:open'],\n\n\tdata() {\n\t\treturn {\n\t\t\tinternalOpen: this.open,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Filter actions by their `visible` predicate. Entries without\n\t\t * `visible` are always shown (backwards compatible).\n\t\t *\n\t\t * @return {Array} Visible actions for the current targetItem.\n\t\t */\n\t\tvisibleActions() {\n\t\t\treturn this.actions.filter((action) => {\n\t\t\t\tif (action.visible === undefined) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tif (typeof action.visible === 'function') {\n\t\t\t\t\treturn !!action.visible(this.targetItem)\n\t\t\t\t}\n\t\t\t\treturn !!action.visible\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Visible actions paired with the link each renders as (null for a button).\n\t\t *\n\t\t * @return {Array<{action: object, link: object|null}>}\n\t\t */\n\t\trenderedActions() {\n\t\t\treturn this.visibleActions.map((action) => ({\n\t\t\t\taction,\n\t\t\t\tlink: this.resolveDisabled(action) ? null : resolveItemActionLink(action, this.targetItem, this.$router),\n\t\t\t}))\n\t\t},\n\t},\n\n\twatch: {\n\t\topen(val) {\n\t\t\tthis.internalOpen = val\n\t\t},\n\n\t\tinternalOpen(val) {\n\t\t\t// Description goes ABOVE `@event`, not inline after it:\n\t\t\t// vue-docgen-api's event-name splitter stops at the first `:`, so\n\t\t\t// `@event update:open <description>` is read as one long event NAME\n\t\t\t// and the generated docs show an empty description.\n\t\t\t/**\n\t\t\t * Fired whenever the menu's internal open state flips. Used by\n\t\t\t * callers that bind `v-model:open` to track visibility.\n\t\t\t *\n\t\t\t * @event update:open\n\t\t\t * @type {boolean}\n\t\t\t */\n\t\t\tthis.$emit('update:open', val)\n\n\t\t\tif (val) {\n\t\t\t\tthis.tagPopper()\n\t\t\t\tthis.startOutsideWatch()\n\t\t\t} else {\n\t\t\t\tthis.stopOutsideWatch()\n\t\t\t\t// NcActions' `@closed` never fires (see `onClosed`), so drop the\n\t\t\t\t// \"a context menu is open\" attribute here. The CSS vars stay put\n\t\t\t\t// — the hide animation still needs the transform, and the next\n\t\t\t\t// `open()` overwrites them before the popper shows again.\n\t\t\t\tdocument.documentElement.removeAttribute(CTX_MENU_DATA_ATTR)\n\t\t\t}\n\t\t},\n\t},\n\n\tmounted() {\n\t\t// NcActions renders its own NcButton as the popover trigger; the menu\n\t\t// opens only via right-click (via `:open.sync`), so the button is\n\t\t// offscreen-positioned (see the `.cn-context-menu` rule below).\n\t\t//\n\t\t// Use `inert` rather than `aria-hidden`: when the menu is dismissed\n\t\t// without picking an item, NcPopover's focus-trap restores focus to the\n\t\t// trigger button. With `aria-hidden` that produces a \"Blocked\n\t\t// aria-hidden on an element because its descendant retained focus\"\n\t\t// warning (focused element must not be hidden from AT). `inert` both\n\t\t// hides the button from the accessibility tree *and* makes it\n\t\t// unfocusable, so the trap's focus restore falls back to <body> instead\n\t\t// of landing on a hidden element. (The button never needs focus — the\n\t\t// menu opens via `:open.sync`, not by focusing the trigger.)\n\t\tconst trigger = this.$el?.querySelector('.action-item__menutoggle')\n\t\tif (trigger) {\n\t\t\ttrigger.setAttribute('inert', '')\n\t\t}\n\n\t\t// Silence NcPopover's dev-mode a11y warning for this specific instance.\n\t\t//\n\t\t// NcPopover (when `window.OC?.debug` is on) calls\n\t\t// `tabbable(triggerContainer)[0]` on every open and warns\n\t\t// \"It looks like you are using a custom button as a <NcPopover>\n\t\t// trigger…\" if nothing tabbable is found. We *are* using NcButton —\n\t\t// the warning's escape hatch — but the button is offscreen-positioned\n\t\t// and tabbable's visibility checks won't reliably find it across\n\t\t// browsers (Chromium's `Element.checkVisibility()` excludes some\n\t\t// offscreen / clipped elements). The advice in the warning (\"bind\n\t\t// #trigger slot attrs\") doesn't apply: NcActions controls its own\n\t\t// trigger and doesn't expose a `#trigger` slot to override.\n\t\t//\n\t\t// Neutralising the check on our own NcPopover instance (one level\n\t\t// down inside NcActions) leaves every other popover untouched and\n\t\t// keeps the check live for real custom triggers elsewhere.\n\t\t//\n\t\t// Vue 3: `this.$children` was removed (it hard-errors under @vue/compat\n\t\t// MODE 3 with \"INSTANCE_CHILDREN compat has been disabled\"), so reach the\n\t\t// NcActions instance through an explicit template ref instead.\n\t\tconst ncActions = this.$refs.actions\n\t\tconst ncPopover = ncActions?.$refs?.popover\n\t\tif (ncPopover) {\n\t\t\tncPopover.checkTriggerA11y = () => {}\n\t\t}\n\n\t\tthis.tagPopper()\n\n\t\t// Mounting with `open` already true never trips the watcher.\n\t\tif (this.internalOpen) {\n\t\t\tthis.startOutsideWatch()\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.stopOutsideWatch()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Mark this menu's own popper element so the cursor-positioning CSS in\n\t\t * `src/css/context-menu.css` can target it — and nothing else.\n\t\t *\n\t\t * That CSS used to key on the `<html>` data attribute alone, which\n\t\t * matches *every* `.v-popper__popper` in the document: any other popover\n\t\t * alive while a context menu was open (a table row's actions menu, most\n\t\t * visibly) got dragged to the last right-click coordinates.\n\t\t *\n\t\t * The marker has to be applied imperatively — NcActions hardcodes\n\t\t * `popoverBaseClass: 'action-item__popper'` on its NcPopover and spreads\n\t\t * no `$attrs` onto it, so there is no prop to pass one in through.\n\t\t *\n\t\t * It must be an *attribute*, not a class. floating-vue binds a dynamic\n\t\t * `class` on this element (`--shown` / `--hidden` / `--show-from` / …)\n\t\t * and Vue's `patchClass` assigns `el.className` wholesale, so a class\n\t\t * added here is wiped the first time the popper opens — the rule then\n\t\t * matches nothing and the menu renders at its offscreen trigger. Vue\n\t\t * only patches props present in the vnode, so an attribute it never\n\t\t * renders is left alone.\n\t\t *\n\t\t * The element itself is stable: floating-vue keeps one `$_popperNode`\n\t\t * and `appendChild`s it into the container on first show, so marking it\n\t\t * once at `mounted()` holds for the component's lifetime. Re-running on\n\t\t * open is a cheap, idempotent guard for a not-yet-ready ref chain.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttagPopper() {\n\t\t\tconst el = this.$refs.actions?.$refs?.popover?.getPopoverContentElement?.()\n\t\t\tel?.setAttribute?.(CTX_MENU_POPPER_ATTR, '')\n\t\t},\n\n\t\t/**\n\t\t * Start watching for a press outside the menu. Driven from both the\n\t\t * `internalOpen` watcher and `mounted()`, because a consumer may mount\n\t\t * with `open` already true — in which case there is no state transition\n\t\t * for the watcher to see.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstartOutsideWatch() {\n\t\t\t// nextTick: the contextmenu/click that opened the menu is still\n\t\t\t// propagating and would dismiss it again on the same gesture.\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (this.internalOpen) {\n\t\t\t\t\t// CAPTURE. A canvas that swallows its own mousedown never lets it\n\t\t\t\t\t// bubble to document, so a menu opened over one could not be\n\t\t\t\t\t// dismissed by clicking the canvas — Vue Flow's d3-zoom pane calls\n\t\t\t\t\t// stopImmediatePropagation, which is exactly that shape.\n\t\t\t\t\tdocument.addEventListener('mousedown', this.onDocumentMouseDown, true)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Stop watching for outside presses.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstopOutsideWatch() {\n\t\t\tdocument.removeEventListener('mousedown', this.onDocumentMouseDown, true)\n\t\t},\n\n\t\t/**\n\t\t * Close the menu when the press lands outside it.\n\t\t *\n\t\t * NcActions has to run in `manual-open` mode here (the menu is opened\n\t\t * from a right-click via the `open` prop, not by activating the trigger\n\t\t * button). `@nextcloud/vue` 9 couples that mode to\n\t\t * `noCloseOnClickOutside: this.manualOpen`, and NcPopover derives\n\t\t * `autoHide: !noCloseOnClickOutside && closeOnClickOutside` — so the\n\t\t * popper's own outside-click dismissal is switched off and nothing else\n\t\t * closes the menu. v8 only cleared the popper's open `triggers`, leaving\n\t\t * autoHide intact, which is why dismissing worked before the Vue 3\n\t\t * upgrade. NcActions spreads no `$attrs` onto NcPopover, so the prop\n\t\t * cannot be overridden from outside — hence handling it here.\n\t\t *\n\t\t * @param {MouseEvent} event The document mousedown.\n\t\t * @return {void}\n\t\t */\n\t\tonDocumentMouseDown(event) {\n\t\t\tif (!this.internalOpen || this.isInsideMenu(event.target)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.onClose()\n\t\t},\n\n\t\t/**\n\t\t * Whether a node sits inside the menu's own surface — the custom panel,\n\t\t * or the popper NcActions teleports to `container=\"body\"` (so it is not\n\t\t * inside `this.$el` and `contains()` on the root would miss it).\n\t\t *\n\t\t * @param {EventTarget} target The event target to test.\n\t\t * @return {boolean} True when the target belongs to this menu.\n\t\t */\n\t\tisInsideMenu(target) {\n\t\t\tif (!(target instanceof Node)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.$refs.panel?.contains(target)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tconst content = this.$refs.actions?.$refs?.popover?.getPopoverContentElement?.()\n\t\t\tif (content?.contains(target)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// Fallback for when the popover ref chain is unavailable —\n\t\t\t// `action-item__popper` is the base class NcActions gives its popper.\n\t\t\treturn target instanceof Element && !!target.closest('.action-item__popper')\n\t\t},\n\n\t\tresolveDisabled(action) {\n\t\t\tif (typeof action.disabled === 'function') {\n\t\t\t\treturn action.disabled(this.targetItem)\n\t\t\t}\n\t\t\treturn !!action.disabled\n\t\t},\n\n\t\t/**\n\t\t * Resolve the `title` field on an action descriptor — supports both\n\t\t * a static string and a function `(targetItem) => string`. Returns\n\t\t * undefined when no title is provided so the attribute isn't rendered.\n\t\t *\n\t\t * @param {object} action The action descriptor.\n\t\t * @return {string|undefined} The tooltip text, or undefined.\n\t\t */\n\t\tresolveTitle(action) {\n\t\t\tif (typeof action.title === 'function') {\n\t\t\t\treturn action.title(this.targetItem) || undefined\n\t\t\t}\n\t\t\treturn action.title || undefined\n\t\t},\n\n\t\t/**\n\t\t * Slugify an action label for use in stable `data-testid` selectors.\n\t\t * Lowercase, kebab-case, strip non-alphanumeric. Used solely by the\n\t\t * `:data-testid` binding on NcActionButton — does not affect runtime\n\t\t * behaviour or rendered text.\n\t\t *\n\t\t * @param {string} label The action's display label.\n\t\t * @return {string} kebab-case slug suitable for a testid suffix.\n\t\t */\n\t\tslugifyLabel(label) {\n\t\t\treturn String(label || '')\n\t\t\t\t.toLowerCase()\n\t\t\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t\t\t.replace(/^-+|-+$/g, '')\n\t\t},\n\n\t\tonAction(action) {\n\t\t\tif (action.handler && typeof action.handler === 'function') {\n\t\t\t\taction.handler(this.targetItem)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event action User picked an entry from the menu. The action's own `handler(targetItem)` (when present) ran synchronously before this event fires; the event lets parents observe / log the choice.\n\t\t\t * @type {{ action: string, row: object|null }}\n\t\t\t */\n\t\t\tthis.$emit('action', { action: action.label, row: this.targetItem })\n\t\t},\n\n\t\t/**\n\t\t * A link entry was clicked: route a plain in-app click, leave the rest to\n\t\t * the browser, and emit `action` as a button would. The `handler` is not\n\t\t * called, since navigating is what the link already does. A modified\n\t\t * click opens a new tab, so nothing is emitted in this one.\n\t\t *\n\t\t * @param {object} action The action descriptor.\n\t\t * @param {object} link The resolved link.\n\t\t * @param {MouseEvent} event The click event.\n\t\t */\n\t\tonLinkAction(action, link, event) {\n\t\t\tif (isModifiedClick(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowItemActionLink(event, link, this.$router)\n\t\t\tthis.$emit('action', { action: action.label, row: this.targetItem })\n\t\t},\n\n\t\tonClose() {\n\t\t\tthis.internalOpen = false\n\t\t\tif (this.activePanel) {\n\t\t\t\tthis.$emit('update:activePanel', null)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event close Fired when the menu starts closing (before the popper's hide animation). This is the reliable one — prefer it over `closed`.\n\t\t\t */\n\t\t\tthis.$emit('close')\n\t\t},\n\n\t\t/**\n\t\t * Clear the active panel and return to the default action list.\n\t\t * Exposed to custom panel slots via the `back` scope binding.\n\t\t */\n\t\tback() {\n\t\t\t// Description above the tag — see the note on `update:open`.\n\t\t\t/**\n\t\t\t * Emitted with `null` to clear the active panel — fired by the\n\t\t\t * panel's `back()` binding and whenever the menu closes while a\n\t\t\t * panel is open. Bind with `v-model:active-panel`.\n\t\t\t *\n\t\t\t * @event update:activePanel\n\t\t\t * @type {null}\n\t\t\t */\n\t\t\tthis.$emit('update:activePanel', null)\n\t\t},\n\n\t\t/**\n\t\t * Fires after the popper's hide animation completes.\n\t\t *\n\t\t * IT MUST NOT CLEAR THE CURSOR POSITION, and it used to. This fires LATE\n\t\t * — a whole hide animation late — so a second click on the same target\n\t\t * reopens the menu before it arrives, and the tear-down then strips the\n\t\t * coordinates that reopen had just written. The transform resolves\n\t\t * against undefined vars and the menu lands in the corner, on every open\n\t\t * after the first.\n\t\t *\n\t\t * It was dead code when written: `@nextcloud/vue` 9 bound `onAfterClose`\n\t\t * against an NcPopover that only emitted `afterHide`. 9.11.0 binds\n\t\t * `onAfterHide`, so it runs now, and a tear-down nothing needed became a\n\t\t * bug. Nothing needs it still: positioning is scoped to this menu's own\n\t\t * popper (see `tagPopper`), the data attribute is dropped in the\n\t\t * `internalOpen` watcher, the next `open()` overwrites the vars, and the\n\t\t * composable's unmount hook clears them for good.\n\t\t */\n\t\tonClosed() {\n\t\t\t/**\n\t\t\t * @event closed Intended to fire after the popper's hide animation completes. Does not currently fire — `@nextcloud/vue` 9's NcActions listens for an `afterClose` event NcPopover never emits. Use `close` instead.\n\t\t\t */\n\t\t\tthis.$emit('closed')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* Selector is deliberately two classes deep. `.cn-context-menu` lands on\n NcActions' root, which NcActions itself styles as `.action-item[data-v-…]\n { position: relative; display: inline-block }`. Scoped CSS appends the attr\n to the last compound only, so a bare `.cn-context-menu` tied that rule at\n specificity (0,2,0) and lost on injection order — the trigger stayed\n `position: relative`, so it kept its in-flow box (top/left just offset it\n visually) and showed up as a full-width ~34px-tall phantom row between the\n table and the pagination on CnIndexPage. Adding the parent class makes this\n (0,3,0) so it wins regardless of order. */\n.cn-context-menu-root > .cn-context-menu {\n\t/* Hide the NcActions trigger button — menu opens only via right-click.\n\t Off-screen rather than display:none / visibility:hidden so NcPopover's\n\t a11y check (`tabbable(triggerContainer)[0]`) still finds the NcButton\n\t and the \"custom button as a <NcPopover> trigger\" warning stays quiet.\n\t Avoid `clip` / `clip-path` here — modern browsers' `Element.checkVisibility()`,\n\t which tabbable calls into, treats those as invisible. Pure offscreen\n\t positioning leaves the element fully \"visible\" to layout while being\n\t off the user's screen. */\n\tposition: absolute;\n\ttop: -9999px;\n\tleft: -9999px;\n\topacity: 0;\n\tpointer-events: none;\n}\n\n.cn-context-menu__backdrop {\n\tposition: fixed;\n\tinset: 0;\n\tz-index: 9998;\n}\n\n.cn-context-menu__panel {\n\tposition: fixed;\n\ttop: var(--cn-ctx-menu-y, 0);\n\tleft: var(--cn-ctx-menu-x, 0);\n\tz-index: 9999;\n\tmin-width: 220px;\n\tpadding: 4px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbox-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);\n\toutline: none;\n}\n</style>\n"],"names":["CnIcon"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnContextMenu.vue2.js","sources":["../../../../src/components/CnContextMenu/CnContextMenu.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-context-menu-root\">\n\t\t<NcActions\n\t\t\tv-if=\"!activePanel\"\n\t\t\tref=\"actions\"\n\t\t\tv-model:open=\"internalOpen\"\n\t\t\t:manualOpen=\"true\"\n\t\t\t:forceMenu=\"true\"\n\t\t\tclass=\"cn-context-menu\"\n\t\t\tcontainer=\"body\"\n\t\t\tdata-testid=\"cn-context-menu\"\n\t\t\t@close=\"onClose\"\n\t\t\t@closed=\"onClosed\">\n\t\t\t<!-- Dynamic actions from array prop -->\n\t\t\t<template v-for=\"{ action, link } in renderedActions\" :key=\"actionKey(action)\">\n\t\t\t\t<!-- A navigate-only action is a real link, so it can be middle-clicked, opened in a new tab or copied. -->\n\t\t\t\t<NcActionLink\n\t\t\t\t\tv-if=\"link\"\n\t\t\t\t\t:href=\"link.href\"\n\t\t\t\t\t:target=\"link.target\"\n\t\t\t\t\t:title=\"resolveTitle(action)\"\n\t\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t\t:data-testid=\"actionTestId(action)\"\n\t\t\t\t\tcloseAfterClick\n\t\t\t\t\t@click=\"onLinkAction(action, link, $event)\">\n\t\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ action.label }}\n\t\t\t\t</NcActionLink>\n\t\t\t\t<NcActionButton\n\t\t\t\t\tv-else\n\t\t\t\t\t:title=\"resolveTitle(action)\"\n\t\t\t\t\t:disabled=\"resolveDisabled(action)\"\n\t\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t\t:data-testid=\"actionTestId(action)\"\n\t\t\t\t\tcloseAfterClick\n\t\t\t\t\t@click=\"onAction(action)\">\n\t\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ action.label }}\n\t\t\t\t</NcActionButton>\n\t\t\t</template>\n\n\t\t\t<!--\n\t\t\t@slot default\n\t\t\t@description Custom NcActionButton-family content rendered inside the\n\t\t\tNcActions menu. Use this for hardcoded buttons (Keepiq pattern) when\n\t\t\tthe `actions` prop is empty.\n\t\t-->\n\t\t\t<slot />\n\t\t</NcActions>\n\n\t\t<!-- CUSTOM PANEL — bypass NcActions, render arbitrary slot content\n\t\t anchored at the cursor. The `#panel:<name>` slot may contain any\n\t\t markup (grids, inputs, custom components) without the NcActions\n\t\t child-allowlist filter. -->\n\t\t<template v-if=\"activePanel && internalOpen\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-context-menu__backdrop\"\n\t\t\t\t@click=\"onClose\"\n\t\t\t\t@contextmenu.prevent=\"onClose\" />\n\t\t\t<div\n\t\t\t\tref=\"panel\"\n\t\t\t\tclass=\"cn-context-menu__panel\"\n\t\t\t\trole=\"menu\"\n\t\t\t\ttabindex=\"-1\"\n\t\t\t\tdata-testid=\"cn-context-menu-panel\"\n\t\t\t\t:data-panel=\"activePanel\"\n\t\t\t\t@keydown.esc.stop=\"onClose\"\n\t\t\t\t@click.stop>\n\t\t\t\t<!-- @slot panel:<name> Free-form custom panel content shown when\n\t\t\t\t `activePanel === '<name>'`. Bypasses the NcActions child\n\t\t\t\t allowlist — put any markup here (grids, inputs, custom\n\t\t\t\t components, etc.). The slot name is dynamic: define one\n\t\t\t\t `#panel:<name>` per panel you want to support. -->\n\t\t\t\t<!-- @binding {Function} back Clear `activePanel`, returning to the default action list. -->\n\t\t\t\t<!-- @binding {Function} close Close the entire menu, equivalent to clicking outside. -->\n\t\t\t\t<!-- @binding {*} targetItem The right-clicked item, forwarded from the `targetItem` prop. -->\n\t\t\t\t<slot\n\t\t\t\t\t:name=\"`panel:${activePanel}`\"\n\t\t\t\t\t:back=\"back\"\n\t\t\t\t\t:close=\"onClose\"\n\t\t\t\t\t:targetItem=\"targetItem\" />\n\t\t\t</div>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { NcActionButton, NcActionLink, NcActions } from '@nextcloud/vue'\nimport { CTX_MENU_DATA_ATTR, CTX_MENU_POPPER_ATTR } from '../../composables/useContextMenu.js'\nimport { followItemActionLink, resolveItemActionLink } from '../../utils/actionLink.js'\nimport { isModifiedClick } from '../../utils/linkNavigation.js'\nimport { isRowActionVisible, rowActionKey, rowActionPayload, rowActionTestId, slugifyActionLabel } from '../../utils/rowActionItem.js'\nimport { CnIcon } from '../CnIcon/index.js'\n\n/**\n * CnContextMenu — Right-click context menu wrapper around NcActions.\n *\n * Provides a pre-configured NcActions instance that positions itself at the\n * cursor via the `useContextMenu` composable's CSS custom properties. Accepts\n * an `actions` array for the common dynamic case (like CnRowActions), and a\n * default slot for hardcoded NcActionButton content.\n *\n * Pair with `useContextMenu()` for state management (open/close, target item,\n * cursor positioning). The composable handles the DOM attributes; this component\n * handles the NcActions template boilerplate.\n *\n * Dynamic actions (CnIndexPage pattern).\n * CnIndexPage passes the same per-row list its row actions menu renders, so the two menus always match.\n * ```vue\n * <CnContextMenu\n * v-model:open=\"contextMenuOpen\"\n * :actions=\"rowActionsFor(contextMenuShownRow)\"\n * :target-item=\"contextMenuShownRow\"\n * @action=\"$emit('action', $event)\"\n * @close=\"closeContextMenu\" />\n * ```\n *\n * Custom buttons via slot (Keepiq pattern)\n * ```vue\n * <CnContextMenu\n * v-model:open=\"contextMenuOpen\"\n * @close=\"closeContextMenu\">\n * <NcActionButton close-after-click @click=\"onRename\">\n * <template #icon><PencilIcon :size=\"20\" /></template>\n * Rename\n * </NcActionButton>\n * </CnContextMenu>\n * ```\n */\nexport default {\n\tname: 'CnContextMenu',\n\n\tcomponents: {\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tNcActionLink,\n\t\tCnIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Whether the context menu is open. Bind with `v-model:open` — the `.sync`\n\t\t * modifier it replaces was removed in Vue 3 and binds nothing.\n\t\t * Bind to `useContextMenu().isOpen`.\n\t\t */\n\t\topen: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Action definitions rendered as NcActionButton items.\n\t\t * Same format as CnRowActions: `{ label, icon?, handler?, disabled?, visible?, title?, destructive? }`.\n\t\t * `icon` accepts either a component (rendered directly) or a string —\n\t\t * a string is treated as an MDI name and rendered via `CnIcon` (e.g.\n\t\t * `\"Eye\"`), which lets manifest (JSON) actions declare icons by name.\n\t\t * `visible` (boolean | (targetItem) => boolean) hides the entry when falsy\n\t\t * (default: shown). `title` (string | (targetItem) => string) renders as\n\t\t * a native tooltip — useful for explaining why an entry is disabled.\n\t\t * A locally decidable `visibleWhen` that is false for the target hides the entry too, as in CnRowActions.\n\t\t * `id` is passed in the `action` payload; `builtin` marks a CnIndexPage built-in, whose testid is `cn-action-item-<id>` in every locale.\n\t\t * `href` (URL) or `to` (vue-router location), each a value or a\n\t\t * `(targetItem) => …` function, render the entry as a real link, with\n\t\t * `linkTarget` as its `target`; a link emits `action` but does not call\n\t\t * `handler`. When the entire array is empty (or all entries are filtered\n\t\t * out), only the default slot content is rendered.\n\t\t *\n\t\t * @type {Array<{label: string, id: string, builtin: boolean, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), visibleWhen: object, title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}\n\t\t */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The right-clicked item (row, folder, etc.). Passed to action `handler`\n\t\t * and `disabled` callbacks, and included in the `action` event payload.\n\t\t * Bind to `useContextMenu().targetItem`.\n\t\t */\n\t\ttargetItem: {\n\t\t\ttype: [Object, String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Name of the currently-active custom panel. When set (and the menu is\n\t\t * open), the default NcActions list is replaced by the matching\n\t\t * `#panel:<name>` slot, rendered free-form (no NcActions child filter).\n\t\t * Bind with `v-model:active-panel` — the component emits `update:activePanel(null)` when\n\t\t * the panel's `back()` binding is invoked or the menu closes.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tactivePanel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['action', 'close', 'closed', 'update:activePanel', 'update:open'],\n\n\tdata() {\n\t\treturn {\n\t\t\tinternalOpen: this.open,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Filter actions by their `visibleWhen` and `visible` gates, with the same rule CnRowActions uses.\n\t\t * Entries without either always show.\n\t\t *\n\t\t * @return {Array} Visible actions for the current targetItem.\n\t\t */\n\t\tvisibleActions() {\n\t\t\treturn this.actions.filter((action) => isRowActionVisible(action, this.targetItem))\n\t\t},\n\n\t\t/**\n\t\t * Visible actions paired with the link each renders as (null for a button).\n\t\t *\n\t\t * @return {Array<{action: object, link: object|null}>}\n\t\t */\n\t\trenderedActions() {\n\t\t\treturn this.visibleActions.map((action) => ({\n\t\t\t\taction,\n\t\t\t\tlink: this.resolveDisabled(action) ? null : resolveItemActionLink(action, this.targetItem, this.$router),\n\t\t\t}))\n\t\t},\n\t},\n\n\twatch: {\n\t\topen(val) {\n\t\t\tthis.internalOpen = val\n\t\t},\n\n\t\tinternalOpen(val) {\n\t\t\t// Description goes ABOVE `@event`, not inline after it:\n\t\t\t// vue-docgen-api's event-name splitter stops at the first `:`, so\n\t\t\t// `@event update:open <description>` is read as one long event NAME\n\t\t\t// and the generated docs show an empty description.\n\t\t\t/**\n\t\t\t * Fired whenever the menu's internal open state flips. Used by\n\t\t\t * callers that bind `v-model:open` to track visibility.\n\t\t\t *\n\t\t\t * @event update:open\n\t\t\t * @type {boolean}\n\t\t\t */\n\t\t\tthis.$emit('update:open', val)\n\n\t\t\tif (val) {\n\t\t\t\tthis.tagPopper()\n\t\t\t\tthis.startOutsideWatch()\n\t\t\t} else {\n\t\t\t\tthis.stopOutsideWatch()\n\t\t\t\t// NcActions' `@closed` never fires (see `onClosed`), so drop the\n\t\t\t\t// \"a context menu is open\" attribute here. The CSS vars stay put\n\t\t\t\t// — the hide animation still needs the transform, and the next\n\t\t\t\t// `open()` overwrites them before the popper shows again.\n\t\t\t\tdocument.documentElement.removeAttribute(CTX_MENU_DATA_ATTR)\n\t\t\t}\n\t\t},\n\t},\n\n\tmounted() {\n\t\t// NcActions renders its own NcButton as the popover trigger; the menu\n\t\t// opens only via right-click (via `:open.sync`), so the button is\n\t\t// offscreen-positioned (see the `.cn-context-menu` rule below).\n\t\t//\n\t\t// Use `inert` rather than `aria-hidden`: when the menu is dismissed\n\t\t// without picking an item, NcPopover's focus-trap restores focus to the\n\t\t// trigger button. With `aria-hidden` that produces a \"Blocked\n\t\t// aria-hidden on an element because its descendant retained focus\"\n\t\t// warning (focused element must not be hidden from AT). `inert` both\n\t\t// hides the button from the accessibility tree *and* makes it\n\t\t// unfocusable, so the trap's focus restore falls back to <body> instead\n\t\t// of landing on a hidden element. (The button never needs focus — the\n\t\t// menu opens via `:open.sync`, not by focusing the trigger.)\n\t\tconst trigger = this.$el?.querySelector('.action-item__menutoggle')\n\t\tif (trigger) {\n\t\t\ttrigger.setAttribute('inert', '')\n\t\t}\n\n\t\t// Silence NcPopover's dev-mode a11y warning for this specific instance.\n\t\t//\n\t\t// NcPopover (when `window.OC?.debug` is on) calls\n\t\t// `tabbable(triggerContainer)[0]` on every open and warns\n\t\t// \"It looks like you are using a custom button as a <NcPopover>\n\t\t// trigger…\" if nothing tabbable is found. We *are* using NcButton —\n\t\t// the warning's escape hatch — but the button is offscreen-positioned\n\t\t// and tabbable's visibility checks won't reliably find it across\n\t\t// browsers (Chromium's `Element.checkVisibility()` excludes some\n\t\t// offscreen / clipped elements). The advice in the warning (\"bind\n\t\t// #trigger slot attrs\") doesn't apply: NcActions controls its own\n\t\t// trigger and doesn't expose a `#trigger` slot to override.\n\t\t//\n\t\t// Neutralising the check on our own NcPopover instance (one level\n\t\t// down inside NcActions) leaves every other popover untouched and\n\t\t// keeps the check live for real custom triggers elsewhere.\n\t\t//\n\t\t// Vue 3: `this.$children` was removed (it hard-errors under @vue/compat\n\t\t// MODE 3 with \"INSTANCE_CHILDREN compat has been disabled\"), so reach the\n\t\t// NcActions instance through an explicit template ref instead.\n\t\tconst ncActions = this.$refs.actions\n\t\tconst ncPopover = ncActions?.$refs?.popover\n\t\tif (ncPopover) {\n\t\t\tncPopover.checkTriggerA11y = () => {}\n\t\t}\n\n\t\tthis.tagPopper()\n\n\t\t// Mounting with `open` already true never trips the watcher.\n\t\tif (this.internalOpen) {\n\t\t\tthis.startOutsideWatch()\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.stopOutsideWatch()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Mark this menu's own popper element so the cursor-positioning CSS in\n\t\t * `src/css/context-menu.css` can target it — and nothing else.\n\t\t *\n\t\t * That CSS used to key on the `<html>` data attribute alone, which\n\t\t * matches *every* `.v-popper__popper` in the document: any other popover\n\t\t * alive while a context menu was open (a table row's actions menu, most\n\t\t * visibly) got dragged to the last right-click coordinates.\n\t\t *\n\t\t * The marker has to be applied imperatively — NcActions hardcodes\n\t\t * `popoverBaseClass: 'action-item__popper'` on its NcPopover and spreads\n\t\t * no `$attrs` onto it, so there is no prop to pass one in through.\n\t\t *\n\t\t * It must be an *attribute*, not a class. floating-vue binds a dynamic\n\t\t * `class` on this element (`--shown` / `--hidden` / `--show-from` / …)\n\t\t * and Vue's `patchClass` assigns `el.className` wholesale, so a class\n\t\t * added here is wiped the first time the popper opens — the rule then\n\t\t * matches nothing and the menu renders at its offscreen trigger. Vue\n\t\t * only patches props present in the vnode, so an attribute it never\n\t\t * renders is left alone.\n\t\t *\n\t\t * The element itself is stable: floating-vue keeps one `$_popperNode`\n\t\t * and `appendChild`s it into the container on first show, so marking it\n\t\t * once at `mounted()` holds for the component's lifetime. Re-running on\n\t\t * open is a cheap, idempotent guard for a not-yet-ready ref chain.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttagPopper() {\n\t\t\tconst el = this.$refs.actions?.$refs?.popover?.getPopoverContentElement?.()\n\t\t\tel?.setAttribute?.(CTX_MENU_POPPER_ATTR, '')\n\t\t},\n\n\t\t/**\n\t\t * Start watching for a press outside the menu. Driven from both the\n\t\t * `internalOpen` watcher and `mounted()`, because a consumer may mount\n\t\t * with `open` already true — in which case there is no state transition\n\t\t * for the watcher to see.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstartOutsideWatch() {\n\t\t\t// nextTick: the contextmenu/click that opened the menu is still\n\t\t\t// propagating and would dismiss it again on the same gesture.\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (this.internalOpen) {\n\t\t\t\t\t// CAPTURE. A canvas that swallows its own mousedown never lets it\n\t\t\t\t\t// bubble to document, so a menu opened over one could not be\n\t\t\t\t\t// dismissed by clicking the canvas — Vue Flow's d3-zoom pane calls\n\t\t\t\t\t// stopImmediatePropagation, which is exactly that shape.\n\t\t\t\t\tdocument.addEventListener('mousedown', this.onDocumentMouseDown, true)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Stop watching for outside presses.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstopOutsideWatch() {\n\t\t\tdocument.removeEventListener('mousedown', this.onDocumentMouseDown, true)\n\t\t},\n\n\t\t/**\n\t\t * Close the menu when the press lands outside it.\n\t\t *\n\t\t * NcActions has to run in `manual-open` mode here (the menu is opened\n\t\t * from a right-click via the `open` prop, not by activating the trigger\n\t\t * button). `@nextcloud/vue` 9 couples that mode to\n\t\t * `noCloseOnClickOutside: this.manualOpen`, and NcPopover derives\n\t\t * `autoHide: !noCloseOnClickOutside && closeOnClickOutside` — so the\n\t\t * popper's own outside-click dismissal is switched off and nothing else\n\t\t * closes the menu. v8 only cleared the popper's open `triggers`, leaving\n\t\t * autoHide intact, which is why dismissing worked before the Vue 3\n\t\t * upgrade. NcActions spreads no `$attrs` onto NcPopover, so the prop\n\t\t * cannot be overridden from outside — hence handling it here.\n\t\t *\n\t\t * @param {MouseEvent} event The document mousedown.\n\t\t * @return {void}\n\t\t */\n\t\tonDocumentMouseDown(event) {\n\t\t\tif (!this.internalOpen || this.isInsideMenu(event.target)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.onClose()\n\t\t},\n\n\t\t/**\n\t\t * Whether a node sits inside the menu's own surface — the custom panel,\n\t\t * or the popper NcActions teleports to `container=\"body\"` (so it is not\n\t\t * inside `this.$el` and `contains()` on the root would miss it).\n\t\t *\n\t\t * @param {EventTarget} target The event target to test.\n\t\t * @return {boolean} True when the target belongs to this menu.\n\t\t */\n\t\tisInsideMenu(target) {\n\t\t\tif (!(target instanceof Node)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.$refs.panel?.contains(target)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tconst content = this.$refs.actions?.$refs?.popover?.getPopoverContentElement?.()\n\t\t\tif (content?.contains(target)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// Fallback for when the popover ref chain is unavailable —\n\t\t\t// `action-item__popper` is the base class NcActions gives its popper.\n\t\t\treturn target instanceof Element && !!target.closest('.action-item__popper')\n\t\t},\n\n\t\tresolveDisabled(action) {\n\t\t\tif (typeof action.disabled === 'function') {\n\t\t\t\treturn action.disabled(this.targetItem)\n\t\t\t}\n\t\t\treturn !!action.disabled\n\t\t},\n\n\t\t/**\n\t\t * Resolve the `title` field on an action descriptor — supports both\n\t\t * a static string and a function `(targetItem) => string`. Returns\n\t\t * undefined when no title is provided so the attribute isn't rendered.\n\t\t *\n\t\t * @param {object} action The action descriptor.\n\t\t * @return {string|undefined} The tooltip text, or undefined.\n\t\t */\n\t\tresolveTitle(action) {\n\t\t\tif (typeof action.title === 'function') {\n\t\t\t\treturn action.title(this.targetItem) || undefined\n\t\t\t}\n\t\t\treturn action.title || undefined\n\t\t},\n\n\t\t/**\n\t\t * Slugify an action label for use in stable `data-testid` selectors.\n\t\t * Lowercase, kebab-case, strip non-alphanumeric.\n\t\t *\n\t\t * @param {string} label The action's display label.\n\t\t * @return {string} kebab-case slug suitable for a testid suffix.\n\t\t */\n\t\tslugifyLabel(label) {\n\t\t\treturn slugifyActionLabel(label)\n\t\t},\n\n\t\t/**\n\t\t * The entry's `data-testid`: `cn-action-item-<id>` for a built-in, the label slug otherwise.\n\t\t *\n\t\t * @param {object} action The action descriptor.\n\t\t * @return {string} The testid.\n\t\t */\n\t\tactionTestId(action) {\n\t\t\treturn rowActionTestId(action)\n\t\t},\n\n\t\t/**\n\t\t * The entry's render key: `builtin:<id>` for a built-in, the label otherwise.\n\t\t *\n\t\t * @param {object} action The action descriptor.\n\t\t * @return {string} The key.\n\t\t */\n\t\tactionKey(action) {\n\t\t\treturn rowActionKey(action)\n\t\t},\n\n\t\tonAction(action) {\n\t\t\tif (action.handler && typeof action.handler === 'function') {\n\t\t\t\taction.handler(this.targetItem)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event action User picked an entry from the menu. The action's own `handler(targetItem)` (when present) ran synchronously before this event fires; the event lets parents observe / log the choice. Payload: `action` (the label), the target as `row`, the action's `id` when it has one, and `builtin: true` for a CnIndexPage built-in.\n\t\t\t * @type {{ action: string, row: object|null, id?: string, builtin?: boolean }}\n\t\t\t */\n\t\t\tthis.$emit('action', rowActionPayload(action, this.targetItem))\n\t\t},\n\n\t\t/**\n\t\t * A link entry was clicked: route a plain in-app click, leave the rest to\n\t\t * the browser, and emit `action` as a button would. The `handler` is not\n\t\t * called, since navigating is what the link already does. A modified\n\t\t * click opens a new tab, so nothing is emitted in this one.\n\t\t *\n\t\t * @param {object} action The action descriptor.\n\t\t * @param {object} link The resolved link.\n\t\t * @param {MouseEvent} event The click event.\n\t\t */\n\t\tonLinkAction(action, link, event) {\n\t\t\tif (isModifiedClick(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowItemActionLink(event, link, this.$router)\n\t\t\tthis.$emit('action', rowActionPayload(action, this.targetItem))\n\t\t},\n\n\t\tonClose() {\n\t\t\tthis.internalOpen = false\n\t\t\tif (this.activePanel) {\n\t\t\t\tthis.$emit('update:activePanel', null)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event close Fired when the menu starts closing (before the popper's hide animation). This is the reliable one — prefer it over `closed`.\n\t\t\t */\n\t\t\tthis.$emit('close')\n\t\t},\n\n\t\t/**\n\t\t * Clear the active panel and return to the default action list.\n\t\t * Exposed to custom panel slots via the `back` scope binding.\n\t\t */\n\t\tback() {\n\t\t\t// Description above the tag — see the note on `update:open`.\n\t\t\t/**\n\t\t\t * Emitted with `null` to clear the active panel — fired by the\n\t\t\t * panel's `back()` binding and whenever the menu closes while a\n\t\t\t * panel is open. Bind with `v-model:active-panel`.\n\t\t\t *\n\t\t\t * @event update:activePanel\n\t\t\t * @type {null}\n\t\t\t */\n\t\t\tthis.$emit('update:activePanel', null)\n\t\t},\n\n\t\t/**\n\t\t * Fires after the popper's hide animation completes.\n\t\t *\n\t\t * IT MUST NOT CLEAR THE CURSOR POSITION, and it used to. This fires LATE\n\t\t * — a whole hide animation late — so a second click on the same target\n\t\t * reopens the menu before it arrives, and the tear-down then strips the\n\t\t * coordinates that reopen had just written. The transform resolves\n\t\t * against undefined vars and the menu lands in the corner, on every open\n\t\t * after the first.\n\t\t *\n\t\t * It was dead code when written: `@nextcloud/vue` 9 bound `onAfterClose`\n\t\t * against an NcPopover that only emitted `afterHide`. 9.11.0 binds\n\t\t * `onAfterHide`, so it runs now, and a tear-down nothing needed became a\n\t\t * bug. Nothing needs it still: positioning is scoped to this menu's own\n\t\t * popper (see `tagPopper`), the data attribute is dropped in the\n\t\t * `internalOpen` watcher, the next `open()` overwrites the vars, and the\n\t\t * composable's unmount hook clears them for good.\n\t\t */\n\t\tonClosed() {\n\t\t\t/**\n\t\t\t * @event closed Intended to fire after the popper's hide animation completes. Does not currently fire — `@nextcloud/vue` 9's NcActions listens for an `afterClose` event NcPopover never emits. Use `close` instead.\n\t\t\t */\n\t\t\tthis.$emit('closed')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* Selector is deliberately two classes deep. `.cn-context-menu` lands on\n NcActions' root, which NcActions itself styles as `.action-item[data-v-…]\n { position: relative; display: inline-block }`. Scoped CSS appends the attr\n to the last compound only, so a bare `.cn-context-menu` tied that rule at\n specificity (0,2,0) and lost on injection order — the trigger stayed\n `position: relative`, so it kept its in-flow box (top/left just offset it\n visually) and showed up as a full-width ~34px-tall phantom row between the\n table and the pagination on CnIndexPage. Adding the parent class makes this\n (0,3,0) so it wins regardless of order. */\n.cn-context-menu-root > .cn-context-menu {\n\t/* Hide the NcActions trigger button — menu opens only via right-click.\n\t Off-screen rather than display:none / visibility:hidden so NcPopover's\n\t a11y check (`tabbable(triggerContainer)[0]`) still finds the NcButton\n\t and the \"custom button as a <NcPopover> trigger\" warning stays quiet.\n\t Avoid `clip` / `clip-path` here — modern browsers' `Element.checkVisibility()`,\n\t which tabbable calls into, treats those as invisible. Pure offscreen\n\t positioning leaves the element fully \"visible\" to layout while being\n\t off the user's screen. */\n\tposition: absolute;\n\ttop: -9999px;\n\tleft: -9999px;\n\topacity: 0;\n\tpointer-events: none;\n}\n\n.cn-context-menu__backdrop {\n\tposition: fixed;\n\tinset: 0;\n\tz-index: 9998;\n}\n\n.cn-context-menu__panel {\n\tposition: fixed;\n\ttop: var(--cn-ctx-menu-y, 0);\n\tleft: var(--cn-ctx-menu-x, 0);\n\tz-index: 9999;\n\tmin-width: 220px;\n\tpadding: 4px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbox-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);\n\toutline: none;\n}\n</style>\n"],"names":["CnIcon"],"mappings":"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"}
|
|
@@ -27,7 +27,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
27
27
|
default: withCtx(() => [
|
|
28
28
|
(openBlock(true), createElementBlock(Fragment, null, renderList($options.renderedActions, ({ action, link }) => {
|
|
29
29
|
return (openBlock(), createElementBlock(Fragment, {
|
|
30
|
-
key: action
|
|
30
|
+
key: $options.actionKey(action)
|
|
31
31
|
}, [
|
|
32
32
|
link
|
|
33
33
|
? (openBlock(), createBlock(_component_NcActionLink, {
|
|
@@ -36,7 +36,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
36
36
|
target: link.target,
|
|
37
37
|
title: $options.resolveTitle(action),
|
|
38
38
|
class: normalizeClass({ 'cn-row-action--destructive': action.destructive }),
|
|
39
|
-
"data-testid":
|
|
39
|
+
"data-testid": $options.actionTestId(action),
|
|
40
40
|
closeAfterClick: "",
|
|
41
41
|
onClick: $event => ($options.onLinkAction(action, link, $event))
|
|
42
42
|
}, createSlots({
|
|
@@ -69,7 +69,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
69
69
|
title: $options.resolveTitle(action),
|
|
70
70
|
disabled: $options.resolveDisabled(action),
|
|
71
71
|
class: normalizeClass({ 'cn-row-action--destructive': action.destructive }),
|
|
72
|
-
"data-testid":
|
|
72
|
+
"data-testid": $options.actionTestId(action),
|
|
73
73
|
closeAfterClick: "",
|
|
74
74
|
onClick: $event => ($options.onAction(action))
|
|
75
75
|
}, createSlots({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnContextMenu.vue3.js","sources":["../../../../src/components/CnContextMenu/CnContextMenu.vue?vue&type=template&id=1bef81d7&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-context-menu-root\">\n\t\t<NcActions\n\t\t\tv-if=\"!activePanel\"\n\t\t\tref=\"actions\"\n\t\t\tv-model:open=\"internalOpen\"\n\t\t\t:manualOpen=\"true\"\n\t\t\t:forceMenu=\"true\"\n\t\t\tclass=\"cn-context-menu\"\n\t\t\tcontainer=\"body\"\n\t\t\tdata-testid=\"cn-context-menu\"\n\t\t\t@close=\"onClose\"\n\t\t\t@closed=\"onClosed\">\n\t\t\t<!-- Dynamic actions from array prop -->\n\t\t\t<template v-for=\"{ action, link } in renderedActions\" :key=\"action.label\">\n\t\t\t\t<!-- A navigate-only action is a real link, so it can be middle-clicked, opened in a new tab or copied. -->\n\t\t\t\t<NcActionLink\n\t\t\t\t\tv-if=\"link\"\n\t\t\t\t\t:href=\"link.href\"\n\t\t\t\t\t:target=\"link.target\"\n\t\t\t\t\t:title=\"resolveTitle(action)\"\n\t\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t\t:data-testid=\"`cn-action-item-${slugifyLabel(action.label)}`\"\n\t\t\t\t\tcloseAfterClick\n\t\t\t\t\t@click=\"onLinkAction(action, link, $event)\">\n\t\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ action.label }}\n\t\t\t\t</NcActionLink>\n\t\t\t\t<NcActionButton\n\t\t\t\t\tv-else\n\t\t\t\t\t:title=\"resolveTitle(action)\"\n\t\t\t\t\t:disabled=\"resolveDisabled(action)\"\n\t\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t\t:data-testid=\"`cn-action-item-${slugifyLabel(action.label)}`\"\n\t\t\t\t\tcloseAfterClick\n\t\t\t\t\t@click=\"onAction(action)\">\n\t\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ action.label }}\n\t\t\t\t</NcActionButton>\n\t\t\t</template>\n\n\t\t\t<!--\n\t\t\t@slot default\n\t\t\t@description Custom NcActionButton-family content rendered inside the\n\t\t\tNcActions menu. Use this for hardcoded buttons (Keepiq pattern) when\n\t\t\tthe `actions` prop is empty.\n\t\t-->\n\t\t\t<slot />\n\t\t</NcActions>\n\n\t\t<!-- CUSTOM PANEL — bypass NcActions, render arbitrary slot content\n\t\t anchored at the cursor. The `#panel:<name>` slot may contain any\n\t\t markup (grids, inputs, custom components) without the NcActions\n\t\t child-allowlist filter. -->\n\t\t<template v-if=\"activePanel && internalOpen\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-context-menu__backdrop\"\n\t\t\t\t@click=\"onClose\"\n\t\t\t\t@contextmenu.prevent=\"onClose\" />\n\t\t\t<div\n\t\t\t\tref=\"panel\"\n\t\t\t\tclass=\"cn-context-menu__panel\"\n\t\t\t\trole=\"menu\"\n\t\t\t\ttabindex=\"-1\"\n\t\t\t\tdata-testid=\"cn-context-menu-panel\"\n\t\t\t\t:data-panel=\"activePanel\"\n\t\t\t\t@keydown.esc.stop=\"onClose\"\n\t\t\t\t@click.stop>\n\t\t\t\t<!-- @slot panel:<name> Free-form custom panel content shown when\n\t\t\t\t `activePanel === '<name>'`. Bypasses the NcActions child\n\t\t\t\t allowlist — put any markup here (grids, inputs, custom\n\t\t\t\t components, etc.). The slot name is dynamic: define one\n\t\t\t\t `#panel:<name>` per panel you want to support. -->\n\t\t\t\t<!-- @binding {Function} back Clear `activePanel`, returning to the default action list. -->\n\t\t\t\t<!-- @binding {Function} close Close the entire menu, equivalent to clicking outside. -->\n\t\t\t\t<!-- @binding {*} targetItem The right-clicked item, forwarded from the `targetItem` prop. -->\n\t\t\t\t<slot\n\t\t\t\t\t:name=\"`panel:${activePanel}`\"\n\t\t\t\t\t:back=\"back\"\n\t\t\t\t\t:close=\"onClose\"\n\t\t\t\t\t:targetItem=\"targetItem\" />\n\t\t\t</div>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { NcActionButton, NcActionLink, NcActions } from '@nextcloud/vue'\nimport { CTX_MENU_DATA_ATTR, CTX_MENU_POPPER_ATTR } from '../../composables/useContextMenu.js'\nimport { followItemActionLink, resolveItemActionLink } from '../../utils/actionLink.js'\nimport { isModifiedClick } from '../../utils/linkNavigation.js'\nimport { CnIcon } from '../CnIcon/index.js'\n\n/**\n * CnContextMenu — Right-click context menu wrapper around NcActions.\n *\n * Provides a pre-configured NcActions instance that positions itself at the\n * cursor via the `useContextMenu` composable's CSS custom properties. Accepts\n * an `actions` array for the common dynamic case (like CnRowActions), and a\n * default slot for hardcoded NcActionButton content.\n *\n * Pair with `useContextMenu()` for state management (open/close, target item,\n * cursor positioning). The composable handles the DOM attributes; this component\n * handles the NcActions template boilerplate.\n *\n * Dynamic actions (CnIndexPage pattern)\n * ```vue\n * <CnContextMenu\n * v-model:open=\"contextMenuOpen\"\n * :actions=\"mergedActions\"\n * :target-item=\"contextMenuRow\"\n * @action=\"$emit('action', $event)\"\n * @close=\"closeContextMenu\" />\n * ```\n *\n * Custom buttons via slot (Keepiq pattern)\n * ```vue\n * <CnContextMenu\n * v-model:open=\"contextMenuOpen\"\n * @close=\"closeContextMenu\">\n * <NcActionButton close-after-click @click=\"onRename\">\n * <template #icon><PencilIcon :size=\"20\" /></template>\n * Rename\n * </NcActionButton>\n * </CnContextMenu>\n * ```\n */\nexport default {\n\tname: 'CnContextMenu',\n\n\tcomponents: {\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tNcActionLink,\n\t\tCnIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Whether the context menu is open. Bind with `v-model:open` — the `.sync`\n\t\t * modifier it replaces was removed in Vue 3 and binds nothing.\n\t\t * Bind to `useContextMenu().isOpen`.\n\t\t */\n\t\topen: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Action definitions rendered as NcActionButton items.\n\t\t * Same format as CnRowActions: `{ label, icon?, handler?, disabled?, visible?, title?, destructive? }`.\n\t\t * `icon` accepts either a component (rendered directly) or a string —\n\t\t * a string is treated as an MDI name and rendered via `CnIcon` (e.g.\n\t\t * `\"Eye\"`), which lets manifest (JSON) actions declare icons by name.\n\t\t * `visible` (boolean | (targetItem) => boolean) hides the entry when falsy\n\t\t * (default: shown). `title` (string | (targetItem) => string) renders as\n\t\t * a native tooltip — useful for explaining why an entry is disabled.\n\t\t * `href` (URL) or `to` (vue-router location), each a value or a\n\t\t * `(targetItem) => …` function, render the entry as a real link, with\n\t\t * `linkTarget` as its `target`; a link emits `action` but does not call\n\t\t * `handler`. When the entire array is empty (or all entries are filtered\n\t\t * out), only the default slot content is rendered.\n\t\t *\n\t\t * @type {Array<{label: string, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}\n\t\t */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The right-clicked item (row, folder, etc.). Passed to action `handler`\n\t\t * and `disabled` callbacks, and included in the `action` event payload.\n\t\t * Bind to `useContextMenu().targetItem`.\n\t\t */\n\t\ttargetItem: {\n\t\t\ttype: [Object, String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Name of the currently-active custom panel. When set (and the menu is\n\t\t * open), the default NcActions list is replaced by the matching\n\t\t * `#panel:<name>` slot, rendered free-form (no NcActions child filter).\n\t\t * Bind with `v-model:active-panel` — the component emits `update:activePanel(null)` when\n\t\t * the panel's `back()` binding is invoked or the menu closes.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tactivePanel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['action', 'close', 'closed', 'update:activePanel', 'update:open'],\n\n\tdata() {\n\t\treturn {\n\t\t\tinternalOpen: this.open,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Filter actions by their `visible` predicate. Entries without\n\t\t * `visible` are always shown (backwards compatible).\n\t\t *\n\t\t * @return {Array} Visible actions for the current targetItem.\n\t\t */\n\t\tvisibleActions() {\n\t\t\treturn this.actions.filter((action) => {\n\t\t\t\tif (action.visible === undefined) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tif (typeof action.visible === 'function') {\n\t\t\t\t\treturn !!action.visible(this.targetItem)\n\t\t\t\t}\n\t\t\t\treturn !!action.visible\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Visible actions paired with the link each renders as (null for a button).\n\t\t *\n\t\t * @return {Array<{action: object, link: object|null}>}\n\t\t */\n\t\trenderedActions() {\n\t\t\treturn this.visibleActions.map((action) => ({\n\t\t\t\taction,\n\t\t\t\tlink: this.resolveDisabled(action) ? null : resolveItemActionLink(action, this.targetItem, this.$router),\n\t\t\t}))\n\t\t},\n\t},\n\n\twatch: {\n\t\topen(val) {\n\t\t\tthis.internalOpen = val\n\t\t},\n\n\t\tinternalOpen(val) {\n\t\t\t// Description goes ABOVE `@event`, not inline after it:\n\t\t\t// vue-docgen-api's event-name splitter stops at the first `:`, so\n\t\t\t// `@event update:open <description>` is read as one long event NAME\n\t\t\t// and the generated docs show an empty description.\n\t\t\t/**\n\t\t\t * Fired whenever the menu's internal open state flips. Used by\n\t\t\t * callers that bind `v-model:open` to track visibility.\n\t\t\t *\n\t\t\t * @event update:open\n\t\t\t * @type {boolean}\n\t\t\t */\n\t\t\tthis.$emit('update:open', val)\n\n\t\t\tif (val) {\n\t\t\t\tthis.tagPopper()\n\t\t\t\tthis.startOutsideWatch()\n\t\t\t} else {\n\t\t\t\tthis.stopOutsideWatch()\n\t\t\t\t// NcActions' `@closed` never fires (see `onClosed`), so drop the\n\t\t\t\t// \"a context menu is open\" attribute here. The CSS vars stay put\n\t\t\t\t// — the hide animation still needs the transform, and the next\n\t\t\t\t// `open()` overwrites them before the popper shows again.\n\t\t\t\tdocument.documentElement.removeAttribute(CTX_MENU_DATA_ATTR)\n\t\t\t}\n\t\t},\n\t},\n\n\tmounted() {\n\t\t// NcActions renders its own NcButton as the popover trigger; the menu\n\t\t// opens only via right-click (via `:open.sync`), so the button is\n\t\t// offscreen-positioned (see the `.cn-context-menu` rule below).\n\t\t//\n\t\t// Use `inert` rather than `aria-hidden`: when the menu is dismissed\n\t\t// without picking an item, NcPopover's focus-trap restores focus to the\n\t\t// trigger button. With `aria-hidden` that produces a \"Blocked\n\t\t// aria-hidden on an element because its descendant retained focus\"\n\t\t// warning (focused element must not be hidden from AT). `inert` both\n\t\t// hides the button from the accessibility tree *and* makes it\n\t\t// unfocusable, so the trap's focus restore falls back to <body> instead\n\t\t// of landing on a hidden element. (The button never needs focus — the\n\t\t// menu opens via `:open.sync`, not by focusing the trigger.)\n\t\tconst trigger = this.$el?.querySelector('.action-item__menutoggle')\n\t\tif (trigger) {\n\t\t\ttrigger.setAttribute('inert', '')\n\t\t}\n\n\t\t// Silence NcPopover's dev-mode a11y warning for this specific instance.\n\t\t//\n\t\t// NcPopover (when `window.OC?.debug` is on) calls\n\t\t// `tabbable(triggerContainer)[0]` on every open and warns\n\t\t// \"It looks like you are using a custom button as a <NcPopover>\n\t\t// trigger…\" if nothing tabbable is found. We *are* using NcButton —\n\t\t// the warning's escape hatch — but the button is offscreen-positioned\n\t\t// and tabbable's visibility checks won't reliably find it across\n\t\t// browsers (Chromium's `Element.checkVisibility()` excludes some\n\t\t// offscreen / clipped elements). The advice in the warning (\"bind\n\t\t// #trigger slot attrs\") doesn't apply: NcActions controls its own\n\t\t// trigger and doesn't expose a `#trigger` slot to override.\n\t\t//\n\t\t// Neutralising the check on our own NcPopover instance (one level\n\t\t// down inside NcActions) leaves every other popover untouched and\n\t\t// keeps the check live for real custom triggers elsewhere.\n\t\t//\n\t\t// Vue 3: `this.$children` was removed (it hard-errors under @vue/compat\n\t\t// MODE 3 with \"INSTANCE_CHILDREN compat has been disabled\"), so reach the\n\t\t// NcActions instance through an explicit template ref instead.\n\t\tconst ncActions = this.$refs.actions\n\t\tconst ncPopover = ncActions?.$refs?.popover\n\t\tif (ncPopover) {\n\t\t\tncPopover.checkTriggerA11y = () => {}\n\t\t}\n\n\t\tthis.tagPopper()\n\n\t\t// Mounting with `open` already true never trips the watcher.\n\t\tif (this.internalOpen) {\n\t\t\tthis.startOutsideWatch()\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.stopOutsideWatch()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Mark this menu's own popper element so the cursor-positioning CSS in\n\t\t * `src/css/context-menu.css` can target it — and nothing else.\n\t\t *\n\t\t * That CSS used to key on the `<html>` data attribute alone, which\n\t\t * matches *every* `.v-popper__popper` in the document: any other popover\n\t\t * alive while a context menu was open (a table row's actions menu, most\n\t\t * visibly) got dragged to the last right-click coordinates.\n\t\t *\n\t\t * The marker has to be applied imperatively — NcActions hardcodes\n\t\t * `popoverBaseClass: 'action-item__popper'` on its NcPopover and spreads\n\t\t * no `$attrs` onto it, so there is no prop to pass one in through.\n\t\t *\n\t\t * It must be an *attribute*, not a class. floating-vue binds a dynamic\n\t\t * `class` on this element (`--shown` / `--hidden` / `--show-from` / …)\n\t\t * and Vue's `patchClass` assigns `el.className` wholesale, so a class\n\t\t * added here is wiped the first time the popper opens — the rule then\n\t\t * matches nothing and the menu renders at its offscreen trigger. Vue\n\t\t * only patches props present in the vnode, so an attribute it never\n\t\t * renders is left alone.\n\t\t *\n\t\t * The element itself is stable: floating-vue keeps one `$_popperNode`\n\t\t * and `appendChild`s it into the container on first show, so marking it\n\t\t * once at `mounted()` holds for the component's lifetime. Re-running on\n\t\t * open is a cheap, idempotent guard for a not-yet-ready ref chain.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttagPopper() {\n\t\t\tconst el = this.$refs.actions?.$refs?.popover?.getPopoverContentElement?.()\n\t\t\tel?.setAttribute?.(CTX_MENU_POPPER_ATTR, '')\n\t\t},\n\n\t\t/**\n\t\t * Start watching for a press outside the menu. Driven from both the\n\t\t * `internalOpen` watcher and `mounted()`, because a consumer may mount\n\t\t * with `open` already true — in which case there is no state transition\n\t\t * for the watcher to see.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstartOutsideWatch() {\n\t\t\t// nextTick: the contextmenu/click that opened the menu is still\n\t\t\t// propagating and would dismiss it again on the same gesture.\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (this.internalOpen) {\n\t\t\t\t\t// CAPTURE. A canvas that swallows its own mousedown never lets it\n\t\t\t\t\t// bubble to document, so a menu opened over one could not be\n\t\t\t\t\t// dismissed by clicking the canvas — Vue Flow's d3-zoom pane calls\n\t\t\t\t\t// stopImmediatePropagation, which is exactly that shape.\n\t\t\t\t\tdocument.addEventListener('mousedown', this.onDocumentMouseDown, true)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Stop watching for outside presses.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstopOutsideWatch() {\n\t\t\tdocument.removeEventListener('mousedown', this.onDocumentMouseDown, true)\n\t\t},\n\n\t\t/**\n\t\t * Close the menu when the press lands outside it.\n\t\t *\n\t\t * NcActions has to run in `manual-open` mode here (the menu is opened\n\t\t * from a right-click via the `open` prop, not by activating the trigger\n\t\t * button). `@nextcloud/vue` 9 couples that mode to\n\t\t * `noCloseOnClickOutside: this.manualOpen`, and NcPopover derives\n\t\t * `autoHide: !noCloseOnClickOutside && closeOnClickOutside` — so the\n\t\t * popper's own outside-click dismissal is switched off and nothing else\n\t\t * closes the menu. v8 only cleared the popper's open `triggers`, leaving\n\t\t * autoHide intact, which is why dismissing worked before the Vue 3\n\t\t * upgrade. NcActions spreads no `$attrs` onto NcPopover, so the prop\n\t\t * cannot be overridden from outside — hence handling it here.\n\t\t *\n\t\t * @param {MouseEvent} event The document mousedown.\n\t\t * @return {void}\n\t\t */\n\t\tonDocumentMouseDown(event) {\n\t\t\tif (!this.internalOpen || this.isInsideMenu(event.target)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.onClose()\n\t\t},\n\n\t\t/**\n\t\t * Whether a node sits inside the menu's own surface — the custom panel,\n\t\t * or the popper NcActions teleports to `container=\"body\"` (so it is not\n\t\t * inside `this.$el` and `contains()` on the root would miss it).\n\t\t *\n\t\t * @param {EventTarget} target The event target to test.\n\t\t * @return {boolean} True when the target belongs to this menu.\n\t\t */\n\t\tisInsideMenu(target) {\n\t\t\tif (!(target instanceof Node)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.$refs.panel?.contains(target)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tconst content = this.$refs.actions?.$refs?.popover?.getPopoverContentElement?.()\n\t\t\tif (content?.contains(target)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// Fallback for when the popover ref chain is unavailable —\n\t\t\t// `action-item__popper` is the base class NcActions gives its popper.\n\t\t\treturn target instanceof Element && !!target.closest('.action-item__popper')\n\t\t},\n\n\t\tresolveDisabled(action) {\n\t\t\tif (typeof action.disabled === 'function') {\n\t\t\t\treturn action.disabled(this.targetItem)\n\t\t\t}\n\t\t\treturn !!action.disabled\n\t\t},\n\n\t\t/**\n\t\t * Resolve the `title` field on an action descriptor — supports both\n\t\t * a static string and a function `(targetItem) => string`. Returns\n\t\t * undefined when no title is provided so the attribute isn't rendered.\n\t\t *\n\t\t * @param {object} action The action descriptor.\n\t\t * @return {string|undefined} The tooltip text, or undefined.\n\t\t */\n\t\tresolveTitle(action) {\n\t\t\tif (typeof action.title === 'function') {\n\t\t\t\treturn action.title(this.targetItem) || undefined\n\t\t\t}\n\t\t\treturn action.title || undefined\n\t\t},\n\n\t\t/**\n\t\t * Slugify an action label for use in stable `data-testid` selectors.\n\t\t * Lowercase, kebab-case, strip non-alphanumeric. Used solely by the\n\t\t * `:data-testid` binding on NcActionButton — does not affect runtime\n\t\t * behaviour or rendered text.\n\t\t *\n\t\t * @param {string} label The action's display label.\n\t\t * @return {string} kebab-case slug suitable for a testid suffix.\n\t\t */\n\t\tslugifyLabel(label) {\n\t\t\treturn String(label || '')\n\t\t\t\t.toLowerCase()\n\t\t\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t\t\t.replace(/^-+|-+$/g, '')\n\t\t},\n\n\t\tonAction(action) {\n\t\t\tif (action.handler && typeof action.handler === 'function') {\n\t\t\t\taction.handler(this.targetItem)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event action User picked an entry from the menu. The action's own `handler(targetItem)` (when present) ran synchronously before this event fires; the event lets parents observe / log the choice.\n\t\t\t * @type {{ action: string, row: object|null }}\n\t\t\t */\n\t\t\tthis.$emit('action', { action: action.label, row: this.targetItem })\n\t\t},\n\n\t\t/**\n\t\t * A link entry was clicked: route a plain in-app click, leave the rest to\n\t\t * the browser, and emit `action` as a button would. The `handler` is not\n\t\t * called, since navigating is what the link already does. A modified\n\t\t * click opens a new tab, so nothing is emitted in this one.\n\t\t *\n\t\t * @param {object} action The action descriptor.\n\t\t * @param {object} link The resolved link.\n\t\t * @param {MouseEvent} event The click event.\n\t\t */\n\t\tonLinkAction(action, link, event) {\n\t\t\tif (isModifiedClick(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowItemActionLink(event, link, this.$router)\n\t\t\tthis.$emit('action', { action: action.label, row: this.targetItem })\n\t\t},\n\n\t\tonClose() {\n\t\t\tthis.internalOpen = false\n\t\t\tif (this.activePanel) {\n\t\t\t\tthis.$emit('update:activePanel', null)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event close Fired when the menu starts closing (before the popper's hide animation). This is the reliable one — prefer it over `closed`.\n\t\t\t */\n\t\t\tthis.$emit('close')\n\t\t},\n\n\t\t/**\n\t\t * Clear the active panel and return to the default action list.\n\t\t * Exposed to custom panel slots via the `back` scope binding.\n\t\t */\n\t\tback() {\n\t\t\t// Description above the tag — see the note on `update:open`.\n\t\t\t/**\n\t\t\t * Emitted with `null` to clear the active panel — fired by the\n\t\t\t * panel's `back()` binding and whenever the menu closes while a\n\t\t\t * panel is open. Bind with `v-model:active-panel`.\n\t\t\t *\n\t\t\t * @event update:activePanel\n\t\t\t * @type {null}\n\t\t\t */\n\t\t\tthis.$emit('update:activePanel', null)\n\t\t},\n\n\t\t/**\n\t\t * Fires after the popper's hide animation completes.\n\t\t *\n\t\t * IT MUST NOT CLEAR THE CURSOR POSITION, and it used to. This fires LATE\n\t\t * — a whole hide animation late — so a second click on the same target\n\t\t * reopens the menu before it arrives, and the tear-down then strips the\n\t\t * coordinates that reopen had just written. The transform resolves\n\t\t * against undefined vars and the menu lands in the corner, on every open\n\t\t * after the first.\n\t\t *\n\t\t * It was dead code when written: `@nextcloud/vue` 9 bound `onAfterClose`\n\t\t * against an NcPopover that only emitted `afterHide`. 9.11.0 binds\n\t\t * `onAfterHide`, so it runs now, and a tear-down nothing needed became a\n\t\t * bug. Nothing needs it still: positioning is scoped to this menu's own\n\t\t * popper (see `tagPopper`), the data attribute is dropped in the\n\t\t * `internalOpen` watcher, the next `open()` overwrites the vars, and the\n\t\t * composable's unmount hook clears them for good.\n\t\t */\n\t\tonClosed() {\n\t\t\t/**\n\t\t\t * @event closed Intended to fire after the popper's hide animation completes. Does not currently fire — `@nextcloud/vue` 9's NcActions listens for an `afterClose` event NcPopover never emits. Use `close` instead.\n\t\t\t */\n\t\t\tthis.$emit('closed')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* Selector is deliberately two classes deep. `.cn-context-menu` lands on\n NcActions' root, which NcActions itself styles as `.action-item[data-v-…]\n { position: relative; display: inline-block }`. Scoped CSS appends the attr\n to the last compound only, so a bare `.cn-context-menu` tied that rule at\n specificity (0,2,0) and lost on injection order — the trigger stayed\n `position: relative`, so it kept its in-flow box (top/left just offset it\n visually) and showed up as a full-width ~34px-tall phantom row between the\n table and the pagination on CnIndexPage. Adding the parent class makes this\n (0,3,0) so it wins regardless of order. */\n.cn-context-menu-root > .cn-context-menu {\n\t/* Hide the NcActions trigger button — menu opens only via right-click.\n\t Off-screen rather than display:none / visibility:hidden so NcPopover's\n\t a11y check (`tabbable(triggerContainer)[0]`) still finds the NcButton\n\t and the \"custom button as a <NcPopover> trigger\" warning stays quiet.\n\t Avoid `clip` / `clip-path` here — modern browsers' `Element.checkVisibility()`,\n\t which tabbable calls into, treats those as invisible. Pure offscreen\n\t positioning leaves the element fully \"visible\" to layout while being\n\t off the user's screen. */\n\tposition: absolute;\n\ttop: -9999px;\n\tleft: -9999px;\n\topacity: 0;\n\tpointer-events: none;\n}\n\n.cn-context-menu__backdrop {\n\tposition: fixed;\n\tinset: 0;\n\tz-index: 9998;\n}\n\n.cn-context-menu__panel {\n\tposition: fixed;\n\ttop: var(--cn-ctx-menu-y, 0);\n\tleft: var(--cn-ctx-menu-x, 0);\n\tz-index: 9999;\n\tmin-width: 220px;\n\tpadding: 4px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbox-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);\n\toutline: none;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createBlock","_Fragment","_renderList","_normalizeClass","_toDisplayString","_resolveDynamicComponent","_renderSlot","_createElementVNode"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnContextMenu.vue3.js","sources":["../../../../src/components/CnContextMenu/CnContextMenu.vue?vue&type=template&id=1bef81d7&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-context-menu-root\">\n\t\t<NcActions\n\t\t\tv-if=\"!activePanel\"\n\t\t\tref=\"actions\"\n\t\t\tv-model:open=\"internalOpen\"\n\t\t\t:manualOpen=\"true\"\n\t\t\t:forceMenu=\"true\"\n\t\t\tclass=\"cn-context-menu\"\n\t\t\tcontainer=\"body\"\n\t\t\tdata-testid=\"cn-context-menu\"\n\t\t\t@close=\"onClose\"\n\t\t\t@closed=\"onClosed\">\n\t\t\t<!-- Dynamic actions from array prop -->\n\t\t\t<template v-for=\"{ action, link } in renderedActions\" :key=\"actionKey(action)\">\n\t\t\t\t<!-- A navigate-only action is a real link, so it can be middle-clicked, opened in a new tab or copied. -->\n\t\t\t\t<NcActionLink\n\t\t\t\t\tv-if=\"link\"\n\t\t\t\t\t:href=\"link.href\"\n\t\t\t\t\t:target=\"link.target\"\n\t\t\t\t\t:title=\"resolveTitle(action)\"\n\t\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t\t:data-testid=\"actionTestId(action)\"\n\t\t\t\t\tcloseAfterClick\n\t\t\t\t\t@click=\"onLinkAction(action, link, $event)\">\n\t\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ action.label }}\n\t\t\t\t</NcActionLink>\n\t\t\t\t<NcActionButton\n\t\t\t\t\tv-else\n\t\t\t\t\t:title=\"resolveTitle(action)\"\n\t\t\t\t\t:disabled=\"resolveDisabled(action)\"\n\t\t\t\t\t:class=\"{ 'cn-row-action--destructive': action.destructive }\"\n\t\t\t\t\t:data-testid=\"actionTestId(action)\"\n\t\t\t\t\tcloseAfterClick\n\t\t\t\t\t@click=\"onAction(action)\">\n\t\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t\t<CnIcon v-if=\"typeof action.icon === 'string'\" :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t<component :is=\"action.icon\" v-else :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ action.label }}\n\t\t\t\t</NcActionButton>\n\t\t\t</template>\n\n\t\t\t<!--\n\t\t\t@slot default\n\t\t\t@description Custom NcActionButton-family content rendered inside the\n\t\t\tNcActions menu. Use this for hardcoded buttons (Keepiq pattern) when\n\t\t\tthe `actions` prop is empty.\n\t\t-->\n\t\t\t<slot />\n\t\t</NcActions>\n\n\t\t<!-- CUSTOM PANEL — bypass NcActions, render arbitrary slot content\n\t\t anchored at the cursor. The `#panel:<name>` slot may contain any\n\t\t markup (grids, inputs, custom components) without the NcActions\n\t\t child-allowlist filter. -->\n\t\t<template v-if=\"activePanel && internalOpen\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-context-menu__backdrop\"\n\t\t\t\t@click=\"onClose\"\n\t\t\t\t@contextmenu.prevent=\"onClose\" />\n\t\t\t<div\n\t\t\t\tref=\"panel\"\n\t\t\t\tclass=\"cn-context-menu__panel\"\n\t\t\t\trole=\"menu\"\n\t\t\t\ttabindex=\"-1\"\n\t\t\t\tdata-testid=\"cn-context-menu-panel\"\n\t\t\t\t:data-panel=\"activePanel\"\n\t\t\t\t@keydown.esc.stop=\"onClose\"\n\t\t\t\t@click.stop>\n\t\t\t\t<!-- @slot panel:<name> Free-form custom panel content shown when\n\t\t\t\t `activePanel === '<name>'`. Bypasses the NcActions child\n\t\t\t\t allowlist — put any markup here (grids, inputs, custom\n\t\t\t\t components, etc.). The slot name is dynamic: define one\n\t\t\t\t `#panel:<name>` per panel you want to support. -->\n\t\t\t\t<!-- @binding {Function} back Clear `activePanel`, returning to the default action list. -->\n\t\t\t\t<!-- @binding {Function} close Close the entire menu, equivalent to clicking outside. -->\n\t\t\t\t<!-- @binding {*} targetItem The right-clicked item, forwarded from the `targetItem` prop. -->\n\t\t\t\t<slot\n\t\t\t\t\t:name=\"`panel:${activePanel}`\"\n\t\t\t\t\t:back=\"back\"\n\t\t\t\t\t:close=\"onClose\"\n\t\t\t\t\t:targetItem=\"targetItem\" />\n\t\t\t</div>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { NcActionButton, NcActionLink, NcActions } from '@nextcloud/vue'\nimport { CTX_MENU_DATA_ATTR, CTX_MENU_POPPER_ATTR } from '../../composables/useContextMenu.js'\nimport { followItemActionLink, resolveItemActionLink } from '../../utils/actionLink.js'\nimport { isModifiedClick } from '../../utils/linkNavigation.js'\nimport { isRowActionVisible, rowActionKey, rowActionPayload, rowActionTestId, slugifyActionLabel } from '../../utils/rowActionItem.js'\nimport { CnIcon } from '../CnIcon/index.js'\n\n/**\n * CnContextMenu — Right-click context menu wrapper around NcActions.\n *\n * Provides a pre-configured NcActions instance that positions itself at the\n * cursor via the `useContextMenu` composable's CSS custom properties. Accepts\n * an `actions` array for the common dynamic case (like CnRowActions), and a\n * default slot for hardcoded NcActionButton content.\n *\n * Pair with `useContextMenu()` for state management (open/close, target item,\n * cursor positioning). The composable handles the DOM attributes; this component\n * handles the NcActions template boilerplate.\n *\n * Dynamic actions (CnIndexPage pattern).\n * CnIndexPage passes the same per-row list its row actions menu renders, so the two menus always match.\n * ```vue\n * <CnContextMenu\n * v-model:open=\"contextMenuOpen\"\n * :actions=\"rowActionsFor(contextMenuShownRow)\"\n * :target-item=\"contextMenuShownRow\"\n * @action=\"$emit('action', $event)\"\n * @close=\"closeContextMenu\" />\n * ```\n *\n * Custom buttons via slot (Keepiq pattern)\n * ```vue\n * <CnContextMenu\n * v-model:open=\"contextMenuOpen\"\n * @close=\"closeContextMenu\">\n * <NcActionButton close-after-click @click=\"onRename\">\n * <template #icon><PencilIcon :size=\"20\" /></template>\n * Rename\n * </NcActionButton>\n * </CnContextMenu>\n * ```\n */\nexport default {\n\tname: 'CnContextMenu',\n\n\tcomponents: {\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tNcActionLink,\n\t\tCnIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Whether the context menu is open. Bind with `v-model:open` — the `.sync`\n\t\t * modifier it replaces was removed in Vue 3 and binds nothing.\n\t\t * Bind to `useContextMenu().isOpen`.\n\t\t */\n\t\topen: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Action definitions rendered as NcActionButton items.\n\t\t * Same format as CnRowActions: `{ label, icon?, handler?, disabled?, visible?, title?, destructive? }`.\n\t\t * `icon` accepts either a component (rendered directly) or a string —\n\t\t * a string is treated as an MDI name and rendered via `CnIcon` (e.g.\n\t\t * `\"Eye\"`), which lets manifest (JSON) actions declare icons by name.\n\t\t * `visible` (boolean | (targetItem) => boolean) hides the entry when falsy\n\t\t * (default: shown). `title` (string | (targetItem) => string) renders as\n\t\t * a native tooltip — useful for explaining why an entry is disabled.\n\t\t * A locally decidable `visibleWhen` that is false for the target hides the entry too, as in CnRowActions.\n\t\t * `id` is passed in the `action` payload; `builtin` marks a CnIndexPage built-in, whose testid is `cn-action-item-<id>` in every locale.\n\t\t * `href` (URL) or `to` (vue-router location), each a value or a\n\t\t * `(targetItem) => …` function, render the entry as a real link, with\n\t\t * `linkTarget` as its `target`; a link emits `action` but does not call\n\t\t * `handler`. When the entire array is empty (or all entries are filtered\n\t\t * out), only the default slot content is rendered.\n\t\t *\n\t\t * @type {Array<{label: string, id: string, builtin: boolean, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), visibleWhen: object, title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}\n\t\t */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The right-clicked item (row, folder, etc.). Passed to action `handler`\n\t\t * and `disabled` callbacks, and included in the `action` event payload.\n\t\t * Bind to `useContextMenu().targetItem`.\n\t\t */\n\t\ttargetItem: {\n\t\t\ttype: [Object, String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Name of the currently-active custom panel. When set (and the menu is\n\t\t * open), the default NcActions list is replaced by the matching\n\t\t * `#panel:<name>` slot, rendered free-form (no NcActions child filter).\n\t\t * Bind with `v-model:active-panel` — the component emits `update:activePanel(null)` when\n\t\t * the panel's `back()` binding is invoked or the menu closes.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tactivePanel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['action', 'close', 'closed', 'update:activePanel', 'update:open'],\n\n\tdata() {\n\t\treturn {\n\t\t\tinternalOpen: this.open,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Filter actions by their `visibleWhen` and `visible` gates, with the same rule CnRowActions uses.\n\t\t * Entries without either always show.\n\t\t *\n\t\t * @return {Array} Visible actions for the current targetItem.\n\t\t */\n\t\tvisibleActions() {\n\t\t\treturn this.actions.filter((action) => isRowActionVisible(action, this.targetItem))\n\t\t},\n\n\t\t/**\n\t\t * Visible actions paired with the link each renders as (null for a button).\n\t\t *\n\t\t * @return {Array<{action: object, link: object|null}>}\n\t\t */\n\t\trenderedActions() {\n\t\t\treturn this.visibleActions.map((action) => ({\n\t\t\t\taction,\n\t\t\t\tlink: this.resolveDisabled(action) ? null : resolveItemActionLink(action, this.targetItem, this.$router),\n\t\t\t}))\n\t\t},\n\t},\n\n\twatch: {\n\t\topen(val) {\n\t\t\tthis.internalOpen = val\n\t\t},\n\n\t\tinternalOpen(val) {\n\t\t\t// Description goes ABOVE `@event`, not inline after it:\n\t\t\t// vue-docgen-api's event-name splitter stops at the first `:`, so\n\t\t\t// `@event update:open <description>` is read as one long event NAME\n\t\t\t// and the generated docs show an empty description.\n\t\t\t/**\n\t\t\t * Fired whenever the menu's internal open state flips. Used by\n\t\t\t * callers that bind `v-model:open` to track visibility.\n\t\t\t *\n\t\t\t * @event update:open\n\t\t\t * @type {boolean}\n\t\t\t */\n\t\t\tthis.$emit('update:open', val)\n\n\t\t\tif (val) {\n\t\t\t\tthis.tagPopper()\n\t\t\t\tthis.startOutsideWatch()\n\t\t\t} else {\n\t\t\t\tthis.stopOutsideWatch()\n\t\t\t\t// NcActions' `@closed` never fires (see `onClosed`), so drop the\n\t\t\t\t// \"a context menu is open\" attribute here. The CSS vars stay put\n\t\t\t\t// — the hide animation still needs the transform, and the next\n\t\t\t\t// `open()` overwrites them before the popper shows again.\n\t\t\t\tdocument.documentElement.removeAttribute(CTX_MENU_DATA_ATTR)\n\t\t\t}\n\t\t},\n\t},\n\n\tmounted() {\n\t\t// NcActions renders its own NcButton as the popover trigger; the menu\n\t\t// opens only via right-click (via `:open.sync`), so the button is\n\t\t// offscreen-positioned (see the `.cn-context-menu` rule below).\n\t\t//\n\t\t// Use `inert` rather than `aria-hidden`: when the menu is dismissed\n\t\t// without picking an item, NcPopover's focus-trap restores focus to the\n\t\t// trigger button. With `aria-hidden` that produces a \"Blocked\n\t\t// aria-hidden on an element because its descendant retained focus\"\n\t\t// warning (focused element must not be hidden from AT). `inert` both\n\t\t// hides the button from the accessibility tree *and* makes it\n\t\t// unfocusable, so the trap's focus restore falls back to <body> instead\n\t\t// of landing on a hidden element. (The button never needs focus — the\n\t\t// menu opens via `:open.sync`, not by focusing the trigger.)\n\t\tconst trigger = this.$el?.querySelector('.action-item__menutoggle')\n\t\tif (trigger) {\n\t\t\ttrigger.setAttribute('inert', '')\n\t\t}\n\n\t\t// Silence NcPopover's dev-mode a11y warning for this specific instance.\n\t\t//\n\t\t// NcPopover (when `window.OC?.debug` is on) calls\n\t\t// `tabbable(triggerContainer)[0]` on every open and warns\n\t\t// \"It looks like you are using a custom button as a <NcPopover>\n\t\t// trigger…\" if nothing tabbable is found. We *are* using NcButton —\n\t\t// the warning's escape hatch — but the button is offscreen-positioned\n\t\t// and tabbable's visibility checks won't reliably find it across\n\t\t// browsers (Chromium's `Element.checkVisibility()` excludes some\n\t\t// offscreen / clipped elements). The advice in the warning (\"bind\n\t\t// #trigger slot attrs\") doesn't apply: NcActions controls its own\n\t\t// trigger and doesn't expose a `#trigger` slot to override.\n\t\t//\n\t\t// Neutralising the check on our own NcPopover instance (one level\n\t\t// down inside NcActions) leaves every other popover untouched and\n\t\t// keeps the check live for real custom triggers elsewhere.\n\t\t//\n\t\t// Vue 3: `this.$children` was removed (it hard-errors under @vue/compat\n\t\t// MODE 3 with \"INSTANCE_CHILDREN compat has been disabled\"), so reach the\n\t\t// NcActions instance through an explicit template ref instead.\n\t\tconst ncActions = this.$refs.actions\n\t\tconst ncPopover = ncActions?.$refs?.popover\n\t\tif (ncPopover) {\n\t\t\tncPopover.checkTriggerA11y = () => {}\n\t\t}\n\n\t\tthis.tagPopper()\n\n\t\t// Mounting with `open` already true never trips the watcher.\n\t\tif (this.internalOpen) {\n\t\t\tthis.startOutsideWatch()\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.stopOutsideWatch()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Mark this menu's own popper element so the cursor-positioning CSS in\n\t\t * `src/css/context-menu.css` can target it — and nothing else.\n\t\t *\n\t\t * That CSS used to key on the `<html>` data attribute alone, which\n\t\t * matches *every* `.v-popper__popper` in the document: any other popover\n\t\t * alive while a context menu was open (a table row's actions menu, most\n\t\t * visibly) got dragged to the last right-click coordinates.\n\t\t *\n\t\t * The marker has to be applied imperatively — NcActions hardcodes\n\t\t * `popoverBaseClass: 'action-item__popper'` on its NcPopover and spreads\n\t\t * no `$attrs` onto it, so there is no prop to pass one in through.\n\t\t *\n\t\t * It must be an *attribute*, not a class. floating-vue binds a dynamic\n\t\t * `class` on this element (`--shown` / `--hidden` / `--show-from` / …)\n\t\t * and Vue's `patchClass` assigns `el.className` wholesale, so a class\n\t\t * added here is wiped the first time the popper opens — the rule then\n\t\t * matches nothing and the menu renders at its offscreen trigger. Vue\n\t\t * only patches props present in the vnode, so an attribute it never\n\t\t * renders is left alone.\n\t\t *\n\t\t * The element itself is stable: floating-vue keeps one `$_popperNode`\n\t\t * and `appendChild`s it into the container on first show, so marking it\n\t\t * once at `mounted()` holds for the component's lifetime. Re-running on\n\t\t * open is a cheap, idempotent guard for a not-yet-ready ref chain.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttagPopper() {\n\t\t\tconst el = this.$refs.actions?.$refs?.popover?.getPopoverContentElement?.()\n\t\t\tel?.setAttribute?.(CTX_MENU_POPPER_ATTR, '')\n\t\t},\n\n\t\t/**\n\t\t * Start watching for a press outside the menu. Driven from both the\n\t\t * `internalOpen` watcher and `mounted()`, because a consumer may mount\n\t\t * with `open` already true — in which case there is no state transition\n\t\t * for the watcher to see.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstartOutsideWatch() {\n\t\t\t// nextTick: the contextmenu/click that opened the menu is still\n\t\t\t// propagating and would dismiss it again on the same gesture.\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (this.internalOpen) {\n\t\t\t\t\t// CAPTURE. A canvas that swallows its own mousedown never lets it\n\t\t\t\t\t// bubble to document, so a menu opened over one could not be\n\t\t\t\t\t// dismissed by clicking the canvas — Vue Flow's d3-zoom pane calls\n\t\t\t\t\t// stopImmediatePropagation, which is exactly that shape.\n\t\t\t\t\tdocument.addEventListener('mousedown', this.onDocumentMouseDown, true)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Stop watching for outside presses.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstopOutsideWatch() {\n\t\t\tdocument.removeEventListener('mousedown', this.onDocumentMouseDown, true)\n\t\t},\n\n\t\t/**\n\t\t * Close the menu when the press lands outside it.\n\t\t *\n\t\t * NcActions has to run in `manual-open` mode here (the menu is opened\n\t\t * from a right-click via the `open` prop, not by activating the trigger\n\t\t * button). `@nextcloud/vue` 9 couples that mode to\n\t\t * `noCloseOnClickOutside: this.manualOpen`, and NcPopover derives\n\t\t * `autoHide: !noCloseOnClickOutside && closeOnClickOutside` — so the\n\t\t * popper's own outside-click dismissal is switched off and nothing else\n\t\t * closes the menu. v8 only cleared the popper's open `triggers`, leaving\n\t\t * autoHide intact, which is why dismissing worked before the Vue 3\n\t\t * upgrade. NcActions spreads no `$attrs` onto NcPopover, so the prop\n\t\t * cannot be overridden from outside — hence handling it here.\n\t\t *\n\t\t * @param {MouseEvent} event The document mousedown.\n\t\t * @return {void}\n\t\t */\n\t\tonDocumentMouseDown(event) {\n\t\t\tif (!this.internalOpen || this.isInsideMenu(event.target)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.onClose()\n\t\t},\n\n\t\t/**\n\t\t * Whether a node sits inside the menu's own surface — the custom panel,\n\t\t * or the popper NcActions teleports to `container=\"body\"` (so it is not\n\t\t * inside `this.$el` and `contains()` on the root would miss it).\n\t\t *\n\t\t * @param {EventTarget} target The event target to test.\n\t\t * @return {boolean} True when the target belongs to this menu.\n\t\t */\n\t\tisInsideMenu(target) {\n\t\t\tif (!(target instanceof Node)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.$refs.panel?.contains(target)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tconst content = this.$refs.actions?.$refs?.popover?.getPopoverContentElement?.()\n\t\t\tif (content?.contains(target)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// Fallback for when the popover ref chain is unavailable —\n\t\t\t// `action-item__popper` is the base class NcActions gives its popper.\n\t\t\treturn target instanceof Element && !!target.closest('.action-item__popper')\n\t\t},\n\n\t\tresolveDisabled(action) {\n\t\t\tif (typeof action.disabled === 'function') {\n\t\t\t\treturn action.disabled(this.targetItem)\n\t\t\t}\n\t\t\treturn !!action.disabled\n\t\t},\n\n\t\t/**\n\t\t * Resolve the `title` field on an action descriptor — supports both\n\t\t * a static string and a function `(targetItem) => string`. Returns\n\t\t * undefined when no title is provided so the attribute isn't rendered.\n\t\t *\n\t\t * @param {object} action The action descriptor.\n\t\t * @return {string|undefined} The tooltip text, or undefined.\n\t\t */\n\t\tresolveTitle(action) {\n\t\t\tif (typeof action.title === 'function') {\n\t\t\t\treturn action.title(this.targetItem) || undefined\n\t\t\t}\n\t\t\treturn action.title || undefined\n\t\t},\n\n\t\t/**\n\t\t * Slugify an action label for use in stable `data-testid` selectors.\n\t\t * Lowercase, kebab-case, strip non-alphanumeric.\n\t\t *\n\t\t * @param {string} label The action's display label.\n\t\t * @return {string} kebab-case slug suitable for a testid suffix.\n\t\t */\n\t\tslugifyLabel(label) {\n\t\t\treturn slugifyActionLabel(label)\n\t\t},\n\n\t\t/**\n\t\t * The entry's `data-testid`: `cn-action-item-<id>` for a built-in, the label slug otherwise.\n\t\t *\n\t\t * @param {object} action The action descriptor.\n\t\t * @return {string} The testid.\n\t\t */\n\t\tactionTestId(action) {\n\t\t\treturn rowActionTestId(action)\n\t\t},\n\n\t\t/**\n\t\t * The entry's render key: `builtin:<id>` for a built-in, the label otherwise.\n\t\t *\n\t\t * @param {object} action The action descriptor.\n\t\t * @return {string} The key.\n\t\t */\n\t\tactionKey(action) {\n\t\t\treturn rowActionKey(action)\n\t\t},\n\n\t\tonAction(action) {\n\t\t\tif (action.handler && typeof action.handler === 'function') {\n\t\t\t\taction.handler(this.targetItem)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event action User picked an entry from the menu. The action's own `handler(targetItem)` (when present) ran synchronously before this event fires; the event lets parents observe / log the choice. Payload: `action` (the label), the target as `row`, the action's `id` when it has one, and `builtin: true` for a CnIndexPage built-in.\n\t\t\t * @type {{ action: string, row: object|null, id?: string, builtin?: boolean }}\n\t\t\t */\n\t\t\tthis.$emit('action', rowActionPayload(action, this.targetItem))\n\t\t},\n\n\t\t/**\n\t\t * A link entry was clicked: route a plain in-app click, leave the rest to\n\t\t * the browser, and emit `action` as a button would. The `handler` is not\n\t\t * called, since navigating is what the link already does. A modified\n\t\t * click opens a new tab, so nothing is emitted in this one.\n\t\t *\n\t\t * @param {object} action The action descriptor.\n\t\t * @param {object} link The resolved link.\n\t\t * @param {MouseEvent} event The click event.\n\t\t */\n\t\tonLinkAction(action, link, event) {\n\t\t\tif (isModifiedClick(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowItemActionLink(event, link, this.$router)\n\t\t\tthis.$emit('action', rowActionPayload(action, this.targetItem))\n\t\t},\n\n\t\tonClose() {\n\t\t\tthis.internalOpen = false\n\t\t\tif (this.activePanel) {\n\t\t\t\tthis.$emit('update:activePanel', null)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event close Fired when the menu starts closing (before the popper's hide animation). This is the reliable one — prefer it over `closed`.\n\t\t\t */\n\t\t\tthis.$emit('close')\n\t\t},\n\n\t\t/**\n\t\t * Clear the active panel and return to the default action list.\n\t\t * Exposed to custom panel slots via the `back` scope binding.\n\t\t */\n\t\tback() {\n\t\t\t// Description above the tag — see the note on `update:open`.\n\t\t\t/**\n\t\t\t * Emitted with `null` to clear the active panel — fired by the\n\t\t\t * panel's `back()` binding and whenever the menu closes while a\n\t\t\t * panel is open. Bind with `v-model:active-panel`.\n\t\t\t *\n\t\t\t * @event update:activePanel\n\t\t\t * @type {null}\n\t\t\t */\n\t\t\tthis.$emit('update:activePanel', null)\n\t\t},\n\n\t\t/**\n\t\t * Fires after the popper's hide animation completes.\n\t\t *\n\t\t * IT MUST NOT CLEAR THE CURSOR POSITION, and it used to. This fires LATE\n\t\t * — a whole hide animation late — so a second click on the same target\n\t\t * reopens the menu before it arrives, and the tear-down then strips the\n\t\t * coordinates that reopen had just written. The transform resolves\n\t\t * against undefined vars and the menu lands in the corner, on every open\n\t\t * after the first.\n\t\t *\n\t\t * It was dead code when written: `@nextcloud/vue` 9 bound `onAfterClose`\n\t\t * against an NcPopover that only emitted `afterHide`. 9.11.0 binds\n\t\t * `onAfterHide`, so it runs now, and a tear-down nothing needed became a\n\t\t * bug. Nothing needs it still: positioning is scoped to this menu's own\n\t\t * popper (see `tagPopper`), the data attribute is dropped in the\n\t\t * `internalOpen` watcher, the next `open()` overwrites the vars, and the\n\t\t * composable's unmount hook clears them for good.\n\t\t */\n\t\tonClosed() {\n\t\t\t/**\n\t\t\t * @event closed Intended to fire after the popper's hide animation completes. Does not currently fire — `@nextcloud/vue` 9's NcActions listens for an `afterClose` event NcPopover never emits. Use `close` instead.\n\t\t\t */\n\t\t\tthis.$emit('closed')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* Selector is deliberately two classes deep. `.cn-context-menu` lands on\n NcActions' root, which NcActions itself styles as `.action-item[data-v-…]\n { position: relative; display: inline-block }`. Scoped CSS appends the attr\n to the last compound only, so a bare `.cn-context-menu` tied that rule at\n specificity (0,2,0) and lost on injection order — the trigger stayed\n `position: relative`, so it kept its in-flow box (top/left just offset it\n visually) and showed up as a full-width ~34px-tall phantom row between the\n table and the pagination on CnIndexPage. Adding the parent class makes this\n (0,3,0) so it wins regardless of order. */\n.cn-context-menu-root > .cn-context-menu {\n\t/* Hide the NcActions trigger button — menu opens only via right-click.\n\t Off-screen rather than display:none / visibility:hidden so NcPopover's\n\t a11y check (`tabbable(triggerContainer)[0]`) still finds the NcButton\n\t and the \"custom button as a <NcPopover> trigger\" warning stays quiet.\n\t Avoid `clip` / `clip-path` here — modern browsers' `Element.checkVisibility()`,\n\t which tabbable calls into, treats those as invisible. Pure offscreen\n\t positioning leaves the element fully \"visible\" to layout while being\n\t off the user's screen. */\n\tposition: absolute;\n\ttop: -9999px;\n\tleft: -9999px;\n\topacity: 0;\n\tpointer-events: none;\n}\n\n.cn-context-menu__backdrop {\n\tposition: fixed;\n\tinset: 0;\n\tz-index: 9998;\n}\n\n.cn-context-menu__panel {\n\tposition: fixed;\n\ttop: var(--cn-ctx-menu-y, 0);\n\tleft: var(--cn-ctx-menu-x, 0);\n\tz-index: 9999;\n\tmin-width: 220px;\n\tpadding: 4px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbox-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);\n\toutline: none;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createBlock","_Fragment","_renderList","_normalizeClass","_toDisplayString","_resolveDynamicComponent","_renderSlot","_createElementVNode"],"mappings":"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"}
|
|
@@ -2134,7 +2134,12 @@ var script = {
|
|
|
2134
2134
|
* the new object. When the referenced schema cannot be loaded, the
|
|
2135
2135
|
* term alone is saved as before, so the field never dead-ends.
|
|
2136
2136
|
*
|
|
2137
|
+
* The form is the one the schema's index page shows for its own Add
|
|
2138
|
+
* button: that page's `createOverride` saves it and its form settings
|
|
2139
|
+
* shape it, with the typed term in the name.
|
|
2140
|
+
*
|
|
2137
2141
|
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
2142
|
+
* @spec openspec/changes/nested-create-uses-page-config/specs/schema-utilities/spec.md
|
|
2138
2143
|
* @param {object} field The reference field descriptor.
|
|
2139
2144
|
* @param {string} term The typed search term.
|
|
2140
2145
|
* @return {Promise<object|null>} The created object, or null.
|
|
@@ -2151,21 +2156,37 @@ var script = {
|
|
|
2151
2156
|
store.registerObjectType(slug, schemaRef, register);
|
|
2152
2157
|
}
|
|
2153
2158
|
const labelField = this.referenceLabelField(field);
|
|
2159
|
+
const loadSchema = async () => {
|
|
2160
|
+
try {
|
|
2161
|
+
return await store.fetchSchema(slug)
|
|
2162
|
+
} catch {
|
|
2163
|
+
return null
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2154
2166
|
// The app may own how this schema is created (pipelinq's client and
|
|
2155
2167
|
// contact need a contact-first endpoint; a plain save 400s). A
|
|
2156
2168
|
// create override keeps this form and saves through the app's
|
|
2157
2169
|
// handler; a create modal (only when no override exists) opens the
|
|
2158
2170
|
// app's own dialog instead of the generic form.
|
|
2159
|
-
|
|
2171
|
+
//
|
|
2172
|
+
// A stored schema usually names its reference by id (`$ref: 28`)
|
|
2173
|
+
// while the manifest page names it by slug (`schema: 'client'`), so
|
|
2174
|
+
// when the reference itself matches no page, the referenced schema
|
|
2175
|
+
// is loaded and its slug, id and uuid are tried too.
|
|
2176
|
+
let schema;
|
|
2177
|
+
let appCreate = this.appCreateFor(register, [schemaRef]);
|
|
2178
|
+
if (!appCreate.page) {
|
|
2179
|
+
schema = await loadSchema();
|
|
2180
|
+
if (schema) {
|
|
2181
|
+
appCreate = this.appCreateFor(register, [schemaRef, schema.slug, schema.id, schema.uuid]);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2160
2184
|
if (!appCreate.override && appCreate.modal) {
|
|
2161
2185
|
return this.openAppCreateModal(appCreate.modal, store, slug, labelField, term)
|
|
2162
2186
|
}
|
|
2163
2187
|
const override = appCreate.override;
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
schema = await store.fetchSchema(slug);
|
|
2167
|
-
} catch {
|
|
2168
|
-
schema = null;
|
|
2188
|
+
if (schema === undefined) {
|
|
2189
|
+
schema = await loadSchema();
|
|
2169
2190
|
}
|
|
2170
2191
|
if (!schema || !schema.properties) {
|
|
2171
2192
|
const payload = { [labelField]: term };
|
|
@@ -2174,36 +2195,84 @@ var script = {
|
|
|
2174
2195
|
: store.saveObject(slug, payload)
|
|
2175
2196
|
}
|
|
2176
2197
|
const initialData = {};
|
|
2177
|
-
|
|
2178
|
-
|
|
2198
|
+
const prefillKey = this.nestedPrefillKey(schema, labelField);
|
|
2199
|
+
if (term && prefillKey) {
|
|
2200
|
+
initialData[prefillKey] = term;
|
|
2179
2201
|
}
|
|
2202
|
+
const form = appCreate.form || this.nestedFormConfig(null);
|
|
2180
2203
|
return new Promise((resolve) => {
|
|
2181
|
-
this.nestedCreate = { field, schema, register, slug, schemaRef, initialData, override, resolve };
|
|
2204
|
+
this.nestedCreate = { field, schema, register, slug, schemaRef, initialData, override, form, resolve };
|
|
2182
2205
|
})
|
|
2183
2206
|
},
|
|
2184
2207
|
|
|
2208
|
+
/**
|
|
2209
|
+
* The property of the referenced schema the typed term belongs in: the
|
|
2210
|
+
* reference's label field when the schema has it, else its name, title
|
|
2211
|
+
* or label. Visibility is decided by the form (a page `fieldOverrides`
|
|
2212
|
+
* entry can make a read-only name editable), not here.
|
|
2213
|
+
*
|
|
2214
|
+
* @spec openspec/changes/nested-create-uses-page-config/specs/schema-utilities/spec.md
|
|
2215
|
+
* @param {object} schema The referenced schema.
|
|
2216
|
+
* @param {string} labelField The reference's label field.
|
|
2217
|
+
* @return {string|null} The property key, or null when none fits.
|
|
2218
|
+
*/
|
|
2219
|
+
nestedPrefillKey(schema, labelField) {
|
|
2220
|
+
const properties = (schema && schema.properties) || {};
|
|
2221
|
+
return [labelField, 'name', 'title', 'label'].find((key) => properties[key]) || null
|
|
2222
|
+
},
|
|
2223
|
+
|
|
2224
|
+
/**
|
|
2225
|
+
* The create-form settings of the index page that lists a schema, in
|
|
2226
|
+
* the shape the nested CnFormDialog binds: the same `excludeFields`,
|
|
2227
|
+
* `includeFields`, `fieldOverrides`, `formSize` and `formColumns`
|
|
2228
|
+
* CnIndexPage hands its own Add form, so creating from a picker shows
|
|
2229
|
+
* the form the page shows. Without a page, the form's defaults.
|
|
2230
|
+
*
|
|
2231
|
+
* @spec openspec/changes/nested-create-uses-page-config/specs/schema-utilities/spec.md
|
|
2232
|
+
* @param {object|null} config The page's `config`, or null.
|
|
2233
|
+
* @return {{excludeFields: string[], includeFields: (string[]|null), fieldOverrides: object, size: string, columns: number}} The form settings.
|
|
2234
|
+
*/
|
|
2235
|
+
nestedFormConfig(config) {
|
|
2236
|
+
const c = config || {};
|
|
2237
|
+
const isMap = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
2238
|
+
return {
|
|
2239
|
+
excludeFields: Array.isArray(c.excludeFields) ? c.excludeFields : [],
|
|
2240
|
+
includeFields: Array.isArray(c.includeFields) ? c.includeFields : null,
|
|
2241
|
+
fieldOverrides: isMap(c.fieldOverrides) ? c.fieldOverrides : {},
|
|
2242
|
+
size: typeof c.formSize === 'string' && c.formSize !== '' ? c.formSize : 'normal',
|
|
2243
|
+
columns: c.formColumns === 2 ? 2 : 1,
|
|
2244
|
+
}
|
|
2245
|
+
},
|
|
2246
|
+
|
|
2185
2247
|
/**
|
|
2186
2248
|
* How the app creates objects of a schema, read off the manifest page
|
|
2187
2249
|
* that lists them: the index page whose `config.schema` (and, when it
|
|
2188
2250
|
* names one, `config.register`) matches. Its `config.createOverride` is
|
|
2189
2251
|
* resolved to the registered handler; its `config.createModal` is the
|
|
2190
|
-
* registry key of a `kind: 'modal'` dialog
|
|
2252
|
+
* registry key of a `kind: 'modal'` dialog; its form settings
|
|
2253
|
+
* (`fieldOverrides` and friends) shape the nested form.
|
|
2191
2254
|
*
|
|
2192
|
-
*
|
|
2255
|
+
* The schema is matched on any of the given references, so a caller
|
|
2256
|
+
* holding the referenced schema can pass its slug, id and uuid: the
|
|
2257
|
+
* manifest names a schema by slug, a stored `$ref` often by id.
|
|
2258
|
+
*
|
|
2259
|
+
* @spec openspec/changes/nested-create-uses-page-config/specs/schema-utilities/spec.md
|
|
2193
2260
|
* @param {string} register The reference's register.
|
|
2194
|
-
* @param {string}
|
|
2195
|
-
* @return {{override: (((formData: object, ctx: object) => Promise<object>)|null), modal: (string|null)}} What the app declared.
|
|
2261
|
+
* @param {string|Array<string|number>} schemaRefs The schema's slug, id or uuid, one or several.
|
|
2262
|
+
* @return {{page: (object|null), override: (((formData: object, ctx: object) => Promise<object>)|null), modal: (string|null), form: (object|null)}} What the app declared.
|
|
2196
2263
|
*/
|
|
2197
|
-
appCreateFor(register,
|
|
2198
|
-
const none = { override: null, modal: null };
|
|
2264
|
+
appCreateFor(register, schemaRefs) {
|
|
2265
|
+
const none = { page: null, override: null, modal: null, form: null };
|
|
2199
2266
|
const pages = this.cnManifest && Array.isArray(this.cnManifest.pages) ? this.cnManifest.pages : [];
|
|
2200
|
-
const wanted =
|
|
2267
|
+
const wanted = (Array.isArray(schemaRefs) ? schemaRefs : [schemaRefs])
|
|
2268
|
+
.filter((ref) => ref !== undefined && ref !== null && ref !== '')
|
|
2269
|
+
.map((ref) => String(ref).toLowerCase());
|
|
2201
2270
|
const page = pages.find((p) => {
|
|
2202
2271
|
const config = p && p.config;
|
|
2203
2272
|
if (!config || config.schema === undefined || config.schema === null) {
|
|
2204
2273
|
return false
|
|
2205
2274
|
}
|
|
2206
|
-
if (String(config.schema).toLowerCase()
|
|
2275
|
+
if (!wanted.includes(String(config.schema).toLowerCase())) {
|
|
2207
2276
|
return false
|
|
2208
2277
|
}
|
|
2209
2278
|
return !config.register || String(config.register) === String(register)
|
|
@@ -2221,7 +2290,7 @@ var script = {
|
|
|
2221
2290
|
const modal = modalEntry && modalEntry.kind === 'modal' && typeof this.cnOpenModal === 'function'
|
|
2222
2291
|
? config.createModal
|
|
2223
2292
|
: null;
|
|
2224
|
-
return { override: override || null, modal }
|
|
2293
|
+
return { page, override: override || null, modal, form: this.nestedFormConfig(config) }
|
|
2225
2294
|
},
|
|
2226
2295
|
|
|
2227
2296
|
/**
|