diff --git a/README.md b/README.md index 91af9399..c8ae4327 100644 --- a/README.md +++ b/README.md @@ -517,10 +517,10 @@ A menu item `MenuItem` can be one of the following types: #### onRenderContextMenu ```ts -onRenderContextMenu(items: ContextMenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean, selection: JSONEditorSelection | null }) : ContextMenuItem[] | undefined +onRenderContextMenu(items: ContextMenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean, selection: JSONEditorSelection | null }) : ContextMenuItem[] | false | undefined ``` -Callback which can be used to make changes to the context menu items. New items can be added, or existing items can be removed or reorganized. When the function returns `undefined`, the original `items` will be applied. Using the context values `mode`, `modal` and `selection`, different actions can be taken depending on the mode of the editor, whether the editor is rendered inside a modal or not and the path of selection. +Callback which can be used to make changes to the context menu items. New items can be added, or existing items can be removed or reorganized. When the function returns `undefined`, the original `items` will be applied and the context menu will be displayed when `readOnly` is `false`. When the function returns `false`, the context menu will never be displayed. Using the context values `mode`, `modal` and `selection`, different actions can be taken depending on the mode of the editor, whether the editor is rendered inside a modal or not and the path of selection. A menu item `ContextMenuItem` can be one of the following types: diff --git a/src/lib/components/modes/JSONEditorRoot.svelte b/src/lib/components/modes/JSONEditorRoot.svelte index 410c5bc5..9fa1be7a 100644 --- a/src/lib/components/modes/JSONEditorRoot.svelte +++ b/src/lib/components/modes/JSONEditorRoot.svelte @@ -122,7 +122,10 @@ $: handleRenderContextMenu = (items: ContextMenuItem[]) => { const itemsOriginal = cloneDeep(items) // the user may change items in the callback - return onRenderContextMenu(items, { mode, modal: insideModal, selection }) || itemsOriginal + return ( + onRenderContextMenu(items, { mode, modal: insideModal, selection }) ?? + (readOnly ? false : itemsOriginal) + ) } export function patch(operations: JSONPatchDocument): JSONPatchResult { diff --git a/src/lib/components/modes/tablemode/TableMode.svelte b/src/lib/components/modes/tablemode/TableMode.svelte index 823328b3..de54e012 100644 --- a/src/lib/components/modes/tablemode/TableMode.svelte +++ b/src/lib/components/modes/tablemode/TableMode.svelte @@ -135,13 +135,15 @@ } from '$lib/logic/actions.js' import JSONRepairModal from '../../modals/JSONRepairModal.svelte' import { resizeObserver } from '$lib/actions/resizeObserver.js' - import TableContextMenu from '../../../components/modes/tablemode/contextmenu/TableContextMenu.svelte' import CopyPasteModal from '../../../components/modals/CopyPasteModal.svelte' import ContextMenuPointer from '../../../components/controls/contextmenu/ContextMenuPointer.svelte' import TableModeWelcome from './TableModeWelcome.svelte' import JSONPreview from '../../controls/JSONPreview.svelte' import RefreshColumnHeader from './RefreshColumnHeader.svelte' import type { Context } from 'svelte-simple-modal' + import type { ContextMenuItem } from '$lib/types' + import createTableContextMenuItems from './contextmenu/createTableContextMenuItems' + import ContextMenu from '../../controls/contextmenu/ContextMenu.svelte' const debug = createDebug('jsoneditor:TableMode') const { open } = getContext('simple-modal') @@ -921,11 +923,10 @@ offsetLeft, showTip }: AbsolutePopupOptions) { - const props = { + const defaultItems: ContextMenuItem[] = createTableContextMenuItems({ json, - documentState: documentState, + documentState, parser, - showTip, onEditValue: handleEditValue, onEditRow: handleEditRow, @@ -937,9 +938,20 @@ onDuplicateRow: handleDuplicateRow, onInsertBeforeRow: handleInsertBeforeRow, onInsertAfterRow: handleInsertAfterRow, - onRemoveRow: handleRemoveRow, + onRemoveRow: handleRemoveRow + }) + + const items = onRenderContextMenu(defaultItems) + + if (items === false) { + return + } - onRenderContextMenu, + const props = { + tip: showTip + ? 'Tip: you can open this context menu via right-click or with Ctrl+Q' + : undefined, + items, onCloseContextMenu: function () { closeAbsolutePopup(popupId) focus() @@ -948,7 +960,7 @@ modalOpen = true - const popupId = openAbsolutePopup(TableContextMenu, props, { + const popupId = openAbsolutePopup(ContextMenu, props, { left, top, offsetTop, @@ -965,7 +977,7 @@ } function handleContextMenu(event: Event) { - if (readOnly || isEditingSelection(documentState.selection)) { + if (isEditingSelection(documentState.selection)) { return } @@ -1014,10 +1026,6 @@ } function handleContextMenuFromTableMenu(event: MouseEvent) { - if (readOnly) { - return - } - openContextMenu({ anchor: findParentWithNodeName(event.target as HTMLElement, 'BUTTON'), offsetTop: 0, diff --git a/src/lib/components/modes/tablemode/contextmenu/TableContextMenu.svelte b/src/lib/components/modes/tablemode/contextmenu/createTableContextMenuItems.ts similarity index 72% rename from src/lib/components/modes/tablemode/contextmenu/TableContextMenu.svelte rename to src/lib/components/modes/tablemode/contextmenu/createTableContextMenuItems.ts index 64ebefda..5c49d140 100644 --- a/src/lib/components/modes/tablemode/contextmenu/TableContextMenu.svelte +++ b/src/lib/components/modes/tablemode/contextmenu/createTableContextMenuItems.ts @@ -1,68 +1,67 @@ - +import type { ContextMenuItem, DocumentState, JSONParser } from 'svelte-jsoneditor' +import { + faCheckSquare, + faClone, + faCopy, + faCut, + faPaste, + faPen, + faPlus, + faSquare, + faTrashCan +} from '@fortawesome/free-solid-svg-icons' +import { isKeySelection, isMultiSelection, isValueSelection } from 'svelte-jsoneditor' +import { compileJSONPointer, getIn } from 'immutable-json-patch' +import { getFocusPath, singleItemSelected } from '$lib/logic/selection' +import { isObjectOrArray } from '$lib/utils/typeUtils' +import { getEnforceString } from '$lib/logic/documentState' - - - +} diff --git a/src/lib/components/modes/treemode/TreeMode.svelte b/src/lib/components/modes/treemode/TreeMode.svelte index e1f551de..ed71eac3 100644 --- a/src/lib/components/modes/treemode/TreeMode.svelte +++ b/src/lib/components/modes/treemode/TreeMode.svelte @@ -61,7 +61,6 @@ getSelectionPaths, getSelectionRight, getSelectionUp, - hasSelectionContents, isAfterSelection, isEditingSelection, isInsideSelection, @@ -73,6 +72,7 @@ isValueSelection, removeEditModeFromSelection, selectAll, + hasSelectionContents, updateSelectionInDocumentState } from '$lib/logic/selection.js' import { mapValidationErrors, validateJSON } from '$lib/logic/validation.js' @@ -102,7 +102,6 @@ import ValidationErrorsOverview from '../../controls/ValidationErrorsOverview.svelte' import CopyPasteModal from '../../modals/CopyPasteModal.svelte' import JSONRepairModal from '../../modals/JSONRepairModal.svelte' - import TreeContextMenu from './contextmenu/TreeContextMenu.svelte' import JSONNode from './JSONNode.svelte' import TreeMenu from './menu/TreeMenu.svelte' import Welcome from './Welcome.svelte' @@ -114,6 +113,7 @@ AfterPatchCallback, Content, ContentErrors, + ConvertType, DocumentState, HistoryItem, InsertType, @@ -161,6 +161,9 @@ } from '$lib/logic/actions.js' import JSONPreview from '../../controls/JSONPreview.svelte' import type { Context } from 'svelte-simple-modal' + import type { ContextMenuItem } from '$lib/types' + import ContextMenu from '../../controls/contextmenu/ContextMenu.svelte' + import createTreeContextMenuItems from './contextmenu/createTreeContextMenuItems' const debug = createDebug('jsoneditor:TreeMode') @@ -951,7 +954,7 @@ readOnly || json === undefined || !documentState.selection || - !hasSelectionContents(documentState.selection) || + !hasSelectionContents || isEmpty(getFocusPath(documentState.selection)) // root selected, cannot duplicate ) { return @@ -1007,7 +1010,7 @@ }) } - function handleInsertFromContextMenu(type: 'value' | 'object' | 'array' | 'structure') { + function handleInsertFromContextMenu(type: InsertType) { if (isKeySelection(documentState.selection)) { // in this case, we do not want to rename the key, but replace the property updateSelection(createValueSelection(documentState.selection.path, false)) @@ -1020,7 +1023,7 @@ handleInsert(type) } - function handleConvert(type: 'value' | 'object' | 'array') { + function handleConvert(type: ConvertType) { if (readOnly || !documentState.selection) { return } @@ -1033,7 +1036,11 @@ try { const path = getAnchorPath(documentState.selection) const currentValue: unknown = getIn(json, path) - const convertedValue = convertValue(currentValue, type, parser) + const convertedValue = convertValue( + currentValue, + type as 'value' | 'object' | 'array', + parser + ) if (convertedValue === currentValue) { // no change, do nothing return @@ -1801,11 +1808,10 @@ offsetLeft, showTip }: AbsolutePopupOptions) { - const props = { + const defaultItems: ContextMenuItem[] = createTreeContextMenuItems({ json, - documentState: documentState, + documentState, parser, - showTip, onEditKey: handleEditKey, onEditValue: handleEditValue, @@ -1821,13 +1827,24 @@ onInsertBefore: handleInsertBefore, onInsert: handleInsertFromContextMenu, - onConvert: handleConvert, onInsertAfter: handleInsertAfter, + onConvert: handleConvert, onSort: handleSortSelection, - onTransform: handleTransformSelection, + onTransform: handleTransformSelection + }) + + const items = onRenderContextMenu(defaultItems) - onRenderContextMenu, + if (items === false) { + return + } + + const props = { + tip: showTip + ? 'Tip: you can open this context menu via right-click or with Ctrl+Q' + : undefined, + items, onCloseContextMenu: function () { closeAbsolutePopup(popupId) focus() @@ -1836,7 +1853,7 @@ modalOpen = true - const popupId = openAbsolutePopup(TreeContextMenu, props, { + const popupId = openAbsolutePopup(ContextMenu, props, { left, top, offsetTop, @@ -1853,7 +1870,7 @@ } function handleContextMenu(event?: Event) { - if (readOnly || isEditingSelection(documentState.selection)) { + if (isEditingSelection(documentState.selection)) { return } @@ -1902,10 +1919,6 @@ } function handleContextMenuFromTreeMenu(event: MouseEvent) { - if (readOnly) { - return - } - openContextMenu({ anchor: findParentWithNodeName(event.target as HTMLElement, 'BUTTON'), offsetTop: 0, diff --git a/src/lib/components/modes/treemode/contextmenu/TreeContextMenu.svelte b/src/lib/components/modes/treemode/contextmenu/createTreeContextMenuItems.ts similarity index 72% rename from src/lib/components/modes/treemode/contextmenu/TreeContextMenu.svelte rename to src/lib/components/modes/treemode/contextmenu/createTreeContextMenuItems.ts index 85292286..8001cc5b 100644 --- a/src/lib/components/modes/treemode/contextmenu/TreeContextMenu.svelte +++ b/src/lib/components/modes/treemode/contextmenu/createTreeContextMenuItems.ts @@ -1,115 +1,124 @@ - +import { + faArrowRightArrowLeft, + faCaretSquareDown, + faCaretSquareUp, + faCheckSquare, + faClone, + faCopy, + faCropAlt, + faCut, + faFilter, + faPaste, + faPen, + faPlus, + faSortAmountDownAlt, + faSquare, + faTrashCan +} from '@fortawesome/free-solid-svg-icons' +import { + canConvert, + getFocusPath, + isKeySelection, + isMultiSelection, + isValueSelection, + singleItemSelected +} from '$lib/logic/selection' +import type { DocumentState, InsertType, JSONParser } from 'svelte-jsoneditor' +import { initial, isEmpty, isObject } from 'lodash-es' +import { compileJSONPointer, getIn } from 'immutable-json-patch' +import { isObjectOrArray } from '$lib/utils/typeUtils' +import { getEnforceString } from '$lib/logic/documentState' +import type { ContextMenuItem } from 'svelte-jsoneditor' - - - +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 38cb62b7..47afbdcd 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -314,8 +314,8 @@ export type RenderContextMenuContext = RenderMenuContext & { selection: JSONEdit export type OnRenderContextMenu = ( items: ContextMenuItem[], context: RenderContextMenuContext -) => ContextMenuItem[] | undefined -export type OnRenderContextMenuInternal = (items: ContextMenuItem[]) => ContextMenuItem[] +) => ContextMenuItem[] | false | undefined +export type OnRenderContextMenuInternal = (items: ContextMenuItem[]) => ContextMenuItem[] | false export type OnError = (error: Error) => void export type OnFocus = () => void export type OnBlur = () => void @@ -392,7 +392,8 @@ export interface HistoryItem { } } -export type InsertType = 'value' | 'object' | 'array' | 'structure' +export type ConvertType = 'value' | 'object' | 'array' +export type InsertType = ConvertType | 'structure' export interface PopupEntry { id: number diff --git a/src/routes/development/+page.svelte b/src/routes/development/+page.svelte index c7a38b9d..b19fd3c2 100644 --- a/src/routes/development/+page.svelte +++ b/src/routes/development/+page.svelte @@ -380,7 +380,7 @@ } function onRenderContextMenu(items: ContextMenuItem[], content: RenderMenuContext) { console.log('onRenderContextMenu', items, content) - return items + return $readOnly ? false : items // This return is equivalent to onRenderContextMenu is undefined } function openInWindow() {