diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-duplicate.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-duplicate.svg
new file mode 100644
index 00000000000..2ade592c087
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-duplicate.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.tsx
index 187a8dee74d..f469ff49ad7 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.tsx
@@ -49,6 +49,11 @@ const BlockEditor = forwardRef(
onChange?.(backendFormat);
},
+ editorProps: {
+ attributes: {
+ class: 'om-block-editor',
+ },
+ },
});
useImperativeHandle(ref, () => ({
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockMenu/BlockMenu.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockMenu/BlockMenu.tsx
new file mode 100644
index 00000000000..fbc5b2b1297
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockMenu/BlockMenu.tsx
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2023 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React, { useCallback, useEffect, useRef } from 'react';
+import tippy, { Instance } from 'tippy.js';
+
+import { Editor } from '@tiptap/react';
+
+import { NodeSelection } from '@tiptap/pm/state';
+import { isUndefined } from 'lodash';
+import { useTranslation } from 'react-i18next';
+import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-delete.svg';
+import { ReactComponent as DuplicateIcon } from '../../../assets/svg/ic-duplicate.svg';
+import {
+ nodeDOMAtCoords,
+ nodePosAtDOM,
+} from '../Extensions/BlockAndDragDrop/helpers';
+import './block-menu.less';
+
+interface BlockMenuProps {
+ editor: Editor;
+}
+
+export const BlockMenu = (props: BlockMenuProps) => {
+ const { t } = useTranslation();
+ const { editor } = props;
+ const { view } = editor;
+ const menuRef = useRef(null);
+ const popup = useRef(null);
+
+ const handleClickBlockHandle = useCallback(
+ (event: MouseEvent) => {
+ const { view: editorView } = editor;
+
+ const node = nodeDOMAtCoords({
+ x: event.clientX + 24 * 4 + 24,
+ y: event.clientY,
+ });
+
+ if (!(node instanceof Element)) {
+ return;
+ }
+
+ const nodePos = nodePosAtDOM(node, editorView);
+ if (isUndefined(nodePos)) {
+ return;
+ }
+ const nodeSelection = NodeSelection.create(editorView.state.doc, nodePos);
+
+ editor
+ .chain()
+ .insertContentAt(
+ nodeSelection.to,
+ { type: 'paragraph' },
+ {
+ updateSelection: true,
+ }
+ )
+ .focus(nodeSelection.to)
+ .run();
+ },
+ [editor]
+ );
+
+ const handleClickDragHandle = useCallback(
+ (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+
+ if (target.matches('[data-block-handle]')) {
+ handleClickBlockHandle(event);
+ }
+
+ if (!target.matches('[data-drag-handle]')) {
+ popup.current?.hide();
+
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ popup.current?.setProps({
+ getReferenceClientRect: () => target.getBoundingClientRect(),
+ });
+
+ popup.current?.show();
+ },
+ [view]
+ );
+
+ const handleKeyDown = () => {
+ popup.current?.hide();
+ };
+
+ const handleDuplicate = useCallback(() => {
+ const { view } = editor;
+ const { state } = view;
+ const { selection } = state;
+
+ editor
+ .chain()
+ .insertContentAt(
+ selection.to,
+ selection.content().content.firstChild?.toJSON(),
+ {
+ updateSelection: true,
+ }
+ )
+ .focus(selection.to)
+ .run();
+
+ popup.current?.hide();
+ }, [editor]);
+
+ const handleDelete = useCallback(() => {
+ editor.commands.deleteSelection();
+ popup.current?.hide();
+ }, [editor]);
+
+ useEffect(() => {
+ if (menuRef.current) {
+ menuRef.current.remove();
+ menuRef.current.style.visibility = 'visible';
+
+ popup.current = tippy(view.dom, {
+ getReferenceClientRect: null,
+ content: menuRef.current,
+ appendTo: 'parent',
+ trigger: 'manual',
+ interactive: true,
+ arrow: false,
+ placement: 'top',
+ hideOnClick: true,
+ onShown: () => {
+ menuRef.current?.focus();
+ },
+ });
+ }
+
+ return () => {
+ popup.current?.destroy();
+ popup.current = null;
+ };
+ }, []);
+
+ useEffect(() => {
+ document.addEventListener('click', handleClickDragHandle);
+ document.addEventListener('keydown', handleKeyDown);
+
+ return () => {
+ document.removeEventListener('click', handleClickDragHandle);
+ document.addEventListener('keydown', handleKeyDown);
+ };
+ }, [handleClickDragHandle, handleKeyDown]);
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default BlockMenu;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockMenu/block-menu.less b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockMenu/block-menu.less
new file mode 100644
index 00000000000..21faa55180f
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockMenu/block-menu.less
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import (reference) url('../../../styles/variables.less');
+
+.tippy-box {
+ max-width: 400px !important;
+}
+
+.block-menu {
+ max-height: 300px;
+ padding: 12px 0px;
+ background-color: @white;
+ border: @global-border;
+ border-radius: @border-radius-base;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+
+ .action {
+ background: none;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ border: none;
+ padding: 4px 12px;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ &:disabled {
+ cursor: not-allowed;
+ }
+
+ &:hover {
+ background-color: @grey-2;
+ }
+
+ .action-icon-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .action-name {
+ font-family: inherit;
+ font-size: 0.95rem;
+ }
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.tsx
index b38e9b30991..8f568cdc4cc 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.tsx
@@ -15,6 +15,7 @@ import { isEmpty, isNil } from 'lodash';
import React, { forwardRef, useImperativeHandle, useState } from 'react';
import tippy, { Instance, Props } from 'tippy.js';
import { EditorSlotsRef } from './BlockEditor.interface';
+import BlockMenu from './BlockMenu/BlockMenu';
import BubbleMenu from './BubbleMenu/BubbleMenu';
import LinkModal, { LinkData } from './LinkModal/LinkModal';
import LinkPopup from './LinkPopup/LinkPopup';
@@ -164,6 +165,7 @@ const EditorSlots = forwardRef(
/>
)}
{menus}
+ {!isNil(editor) && }
>
);
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/BlockAndDragDrop/BlockAndDragDrop.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/BlockAndDragDrop/BlockAndDragDrop.tsx
new file mode 100644
index 00000000000..08b76097ff8
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/BlockAndDragDrop/BlockAndDragDrop.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Extension } from '@tiptap/core';
+import { BlockAndDragHandle } from './BlockAndDragHandle';
+
+export interface BlockAndDragHandleOptions {
+ /**
+ * The width of the drag handle
+ */
+ dragHandleWidth: number;
+ /**
+ * The width of the drag handle
+ */
+ blockHandleWidth: number;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+interface DragAndDropOptions {}
+
+const DragAndDrop = Extension.create({
+ name: 'dragAndDrop',
+
+ addProseMirrorPlugins() {
+ return [
+ BlockAndDragHandle({
+ dragHandleWidth: 24,
+ blockHandleWidth: 24,
+ }),
+ ];
+ },
+});
+
+export default DragAndDrop;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/drag-and-drop.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/BlockAndDragDrop/BlockAndDragHandle.ts
similarity index 52%
rename from openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/drag-and-drop.tsx
rename to openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/BlockAndDragDrop/BlockAndDragHandle.ts
index de4fbb32b51..0de14946f7a 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/drag-and-drop.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/BlockAndDragDrop/BlockAndDragHandle.ts
@@ -10,57 +10,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Extension } from '@tiptap/core';
-
import { NodeSelection, Plugin } from '@tiptap/pm/state';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { EditorView, __serializeForClipboard } from '@tiptap/pm/view';
+import { isUndefined } from 'lodash';
+import { BlockAndDragHandleOptions } from './BlockAndDragDrop';
+import { absoluteRect, nodeDOMAtCoords, nodePosAtDOM } from './helpers';
-export interface DragHandleOptions {
- /**
- * The width of the drag handle
- */
- dragHandleWidth: number;
-}
-const absoluteRect = (node: Element) => {
- const data = node.getBoundingClientRect();
+export const BlockAndDragHandle = (options: BlockAndDragHandleOptions) => {
+ let dragHandleElement: HTMLElement | null = null;
+ let blockHandleElement: HTMLElement | null = null;
- return {
- top: data.top,
- left: data.left,
- width: data.width,
- };
-};
+ // Drag Handle handlers
-const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
- return document
- .elementsFromPoint(coords.x, coords.y)
- .find(
- (elem: Element) =>
- elem.parentElement?.matches?.('.ProseMirror') ||
- elem.matches(
- [
- 'li',
- 'p:not(:first-child)',
- 'pre',
- 'blockquote',
- 'h1, h2, h3, h4, h5, h6',
- ].join(', ')
- )
- );
-};
-
-const nodePosAtDOM = (node: Element, view: EditorView) => {
- const boundingRect = node.getBoundingClientRect();
-
- return view.posAtCoords({
- left: boundingRect.left + 1,
- top: boundingRect.top + 1,
- })?.inside;
-};
-
-const DragHandle = (options: DragHandleOptions) => {
const handleDragStart = (event: DragEvent, view: EditorView) => {
view.focus();
@@ -78,7 +41,7 @@ const DragHandle = (options: DragHandleOptions) => {
}
const nodePos = nodePosAtDOM(node, view);
- if (nodePos == null || nodePos < 0) {
+ if (isUndefined(nodePos)) {
return;
}
@@ -99,7 +62,7 @@ const DragHandle = (options: DragHandleOptions) => {
view.dragging = { slice, move: event.ctrlKey };
};
- const handleClick = (event: MouseEvent, view: EditorView) => {
+ const handleDragClick = (event: MouseEvent, view: EditorView) => {
view.focus();
view.dom.classList.remove('om-node-dragging');
@@ -114,7 +77,7 @@ const DragHandle = (options: DragHandleOptions) => {
}
const nodePos = nodePosAtDOM(node, view);
- if (!nodePos) {
+ if (isUndefined(nodePos)) {
return;
}
@@ -123,8 +86,6 @@ const DragHandle = (options: DragHandleOptions) => {
);
};
- let dragHandleElement: HTMLElement | null = null;
-
const hideDragHandle = () => {
if (dragHandleElement) {
dragHandleElement.classList.add('hidden');
@@ -137,27 +98,128 @@ const DragHandle = (options: DragHandleOptions) => {
}
};
+ const handleMouseMoveForDragHandle = (event: MouseEvent) => {
+ const node = nodeDOMAtCoords({
+ x: event.clientX + 50 + options.dragHandleWidth,
+ y: event.clientY,
+ });
+
+ if (!(node instanceof Element) || node.matches('ul, ol')) {
+ hideDragHandle();
+
+ return;
+ }
+
+ const compStyle = window.getComputedStyle(node);
+ const lineHeight = parseInt(compStyle.lineHeight, 10);
+ const paddingTop = parseInt(compStyle.paddingTop, 10);
+
+ const rect = absoluteRect(node);
+
+ rect.top += (lineHeight - 24) / 2;
+ rect.top += paddingTop;
+ // Li markers
+ if (node.matches('ul:not([data-type=taskList]) li, ol li')) {
+ rect.left -= options.dragHandleWidth;
+ }
+ rect.width = options.dragHandleWidth;
+
+ if (!dragHandleElement) {
+ return;
+ }
+
+ dragHandleElement.style.left = `${rect.left - rect.width}px`;
+ dragHandleElement.style.top = `${rect.top}px`;
+ showDragHandle();
+ };
+
+ // Block Handle handlers
+
+ const hideBlockHandle = () => {
+ if (blockHandleElement) {
+ blockHandleElement.classList.add('hidden');
+ }
+ };
+
+ const showBlockHandle = () => {
+ if (blockHandleElement) {
+ blockHandleElement.classList.remove('hidden');
+ }
+ };
+
+ const handleMouseMoveForBlockHandle = (event: MouseEvent) => {
+ const node = nodeDOMAtCoords({
+ x: event.clientX + options.dragHandleWidth * 4 + options.blockHandleWidth,
+ y: event.clientY,
+ });
+
+ if (!(node instanceof Element) || node.matches('ul, ol')) {
+ hideBlockHandle();
+
+ return;
+ }
+
+ const compStyle = window.getComputedStyle(node);
+ const lineHeight = parseInt(compStyle.lineHeight, 10);
+ const paddingTop = parseInt(compStyle.paddingTop, 10);
+
+ const rect = absoluteRect(node);
+
+ rect.top += (lineHeight - 24) / 2;
+ rect.top += paddingTop;
+ // Li markers
+ if (node.matches('ul:not([data-type=taskList]) li, ol li')) {
+ rect.left -= options.blockHandleWidth;
+ }
+ rect.width = options.blockHandleWidth;
+
+ if (!blockHandleElement) {
+ return;
+ }
+
+ blockHandleElement.style.left = `${
+ rect.left - rect.width - options.blockHandleWidth
+ }px`;
+ blockHandleElement.style.top = `${rect.top}px`;
+ showBlockHandle();
+ };
+
return new Plugin({
view: (view) => {
+ // drag handle initialization
dragHandleElement = document.createElement('div');
dragHandleElement.draggable = true;
dragHandleElement.dataset.dragHandle = '';
+ dragHandleElement.title = 'Drag to move\nClick to open menu';
dragHandleElement.classList.add('om-drag-handle');
dragHandleElement.addEventListener('dragstart', (e) => {
handleDragStart(e, view);
});
dragHandleElement.addEventListener('click', (e) => {
- handleClick(e, view);
+ handleDragClick(e, view);
});
hideDragHandle();
+ // block handle initialization
+ blockHandleElement = document.createElement('div');
+ blockHandleElement.draggable = false;
+ blockHandleElement.dataset.blockHandle = '';
+ blockHandleElement.title = 'Add new node';
+ blockHandleElement.classList.add('om-block-handle');
+
+ hideBlockHandle();
+
view?.dom?.parentElement?.appendChild(dragHandleElement);
+ view?.dom?.parentElement?.appendChild(blockHandleElement);
return {
destroy: () => {
dragHandleElement?.remove?.();
dragHandleElement = null;
+
+ blockHandleElement?.remove?.();
+ blockHandleElement = null;
},
};
},
@@ -167,45 +229,16 @@ const DragHandle = (options: DragHandleOptions) => {
if (!view.editable) {
return;
}
-
- const node = nodeDOMAtCoords({
- x: event.clientX + 50 + options.dragHandleWidth,
- y: event.clientY,
- });
-
- if (!(node instanceof Element) || node.matches('ul, ol')) {
- hideDragHandle();
-
- return;
- }
-
- const compStyle = window.getComputedStyle(node);
- const lineHeight = parseInt(compStyle.lineHeight, 10);
- const paddingTop = parseInt(compStyle.paddingTop, 10);
-
- const rect = absoluteRect(node);
-
- rect.top += (lineHeight - 24) / 2;
- rect.top += paddingTop;
- // Li markers
- if (node.matches('ul:not([data-type=taskList]) li, ol li')) {
- rect.left -= options.dragHandleWidth;
- }
- rect.width = options.dragHandleWidth;
-
- if (!dragHandleElement) {
- return;
- }
-
- dragHandleElement.style.left = `${rect.left - rect.width}px`;
- dragHandleElement.style.top = `${rect.top}px`;
- showDragHandle();
+ handleMouseMoveForDragHandle(event);
+ handleMouseMoveForBlockHandle(event);
},
keydown: () => {
hideDragHandle();
+ hideBlockHandle();
},
mousewheel: () => {
hideDragHandle();
+ hideBlockHandle();
},
// dragging class is used for CSS
dragstart: (view) => {
@@ -221,20 +254,3 @@ const DragHandle = (options: DragHandleOptions) => {
},
});
};
-
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-interface DragAndDropOptions {}
-
-const DragAndDrop = Extension.create({
- name: 'dragAndDrop',
-
- addProseMirrorPlugins() {
- return [
- DragHandle({
- dragHandleWidth: 24,
- }),
- ];
- },
-});
-
-export default DragAndDrop;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/BlockAndDragDrop/helpers.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/BlockAndDragDrop/helpers.ts
new file mode 100644
index 00000000000..27a65511cc7
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/BlockAndDragDrop/helpers.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { EditorView } from '@tiptap/pm/view';
+
+export const absoluteRect = (node: Element) => {
+ const data = node.getBoundingClientRect();
+
+ return {
+ top: data.top,
+ left: data.left,
+ width: data.width,
+ };
+};
+
+export const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
+ return document
+ .elementsFromPoint(coords.x, coords.y)
+ .find(
+ (elem: Element) =>
+ elem.parentElement?.matches?.('.ProseMirror') ||
+ elem.matches(
+ [
+ 'li',
+ 'p:not(:first-child)',
+ 'pre',
+ 'blockquote',
+ 'h1, h2, h3, h4, h5, h6',
+ ].join(', ')
+ )
+ );
+};
+
+export const nodePosAtDOM = (node: Element, view: EditorView) => {
+ const boundingRect = node.getBoundingClientRect();
+
+ return view.posAtCoords({
+ left: boundingRect.left + 1,
+ top: boundingRect.top + 1,
+ })?.inside;
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/focus.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/focus.ts
new file mode 100644
index 00000000000..13911f19046
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/focus.ts
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2023 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Extension } from '@tiptap/core';
+import { Plugin, PluginKey } from '@tiptap/pm/state';
+import { Decoration, DecorationSet } from '@tiptap/pm/view';
+
+export interface FocusOptions {
+ className: string;
+ mode: 'all' | 'deepest' | 'shallowest';
+}
+
+export const Focus = Extension.create({
+ name: 'focus',
+
+ addOptions() {
+ return {
+ className: 'has-focus',
+ mode: 'all',
+ };
+ },
+
+ addProseMirrorPlugins() {
+ return [
+ new Plugin({
+ key: new PluginKey('focus'),
+ props: {
+ decorations: ({ doc, selection }) => {
+ const { isEditable, isFocused } = this.editor;
+ const { anchor } = selection;
+ const decorations: Decoration[] = [];
+
+ if (!isEditable || !isFocused) {
+ return DecorationSet.create(doc, []);
+ }
+
+ // Maximum Levels
+ let maxLevels = 0;
+
+ if (this.options.mode === 'deepest') {
+ doc.descendants((node, pos) => {
+ if (node.isText) {
+ return;
+ }
+
+ const isCurrent =
+ anchor >= pos && anchor <= pos + node.nodeSize - 1;
+
+ if (!isCurrent) {
+ return false;
+ }
+
+ maxLevels += 1;
+
+ return;
+ });
+ }
+
+ // Loop through current
+ let currentLevel = 0;
+
+ doc.descendants((node, pos) => {
+ if (node.isText) {
+ return false;
+ }
+
+ const isCurrent =
+ anchor >= pos && anchor <= pos + node.nodeSize - 1;
+
+ if (!isCurrent) {
+ return false;
+ }
+
+ currentLevel += 1;
+
+ const outOfScope =
+ (this.options.mode === 'deepest' &&
+ maxLevels - currentLevel > 0) ||
+ (this.options.mode === 'shallowest' && currentLevel > 1);
+
+ if (outOfScope) {
+ return this.options.mode === 'deepest';
+ }
+
+ decorations.push(
+ Decoration.node(pos, pos + node.nodeSize, {
+ class: this.options.className,
+ })
+ );
+
+ return;
+ });
+
+ return DecorationSet.create(doc, decorations);
+ },
+ },
+ }),
+ ];
+ },
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/index.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/index.ts
index 8b1e3213428..72db4a0bafa 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/index.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/index.ts
@@ -15,8 +15,9 @@ import Placeholder from '@tiptap/extension-placeholder';
import TaskItem from '@tiptap/extension-task-item';
import TaskList from '@tiptap/extension-task-list';
import StarterKit from '@tiptap/starter-kit';
+import BlockAndDragDrop from './BlockAndDragDrop/BlockAndDragDrop';
import DiffView from './diff-view';
-import DragAndDrop from './drag-and-drop';
+import { Focus } from './focus';
import { Hashtag } from './hashtag';
import { hashtagSuggestion } from './hashtag/hashtagSuggestion';
import { Image } from './image/image';
@@ -102,5 +103,8 @@ export const extensions = [
allowBase64: true,
inline: true,
}),
- DragAndDrop,
+ BlockAndDragDrop,
+ Focus.configure({
+ mode: 'deepest',
+ }),
];
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/index.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/index.ts
index 0c531c0a476..faa754c9f6b 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/index.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/index.ts
@@ -14,6 +14,8 @@ import { Extension } from '@tiptap/core';
import { PluginKey } from '@tiptap/pm/state';
import Suggestion, { SuggestionOptions } from '@tiptap/suggestion';
+export const slashMenuPluginKey = new PluginKey('slashSuggestion');
+
export default Extension.create({
name: 'slashCommand',
@@ -33,7 +35,7 @@ export default Extension.create({
addProseMirrorPlugins() {
return [
Suggestion({
- pluginKey: new PluginKey('slashSuggestion'),
+ pluginKey: slashMenuPluginKey,
...this.options.slashSuggestion,
editor: this.editor,
}),
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/block-editor.less b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/block-editor.less
index 11ef58c563e..a93a883fa4b 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/block-editor.less
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/block-editor.less
@@ -17,8 +17,12 @@
@markdown-bg-color: #f8f8fa;
.block-editor-wrapper {
+ .om-block-editor > p:last-child {
+ // this is to have enough space after last node, referred from the reference editor
+ padding-bottom: 30vh;
+ }
// show placeholder when editor is in focused mode
- .tiptap .is-node-empty:last-child::before {
+ .tiptap.ProseMirror-focused .is-node-empty.has-focus::before {
color: @grey-3;
content: attr(data-placeholder);
float: left;
@@ -250,7 +254,8 @@
box-shadow: none;
}
-.om-drag-handle {
+.om-drag-handle,
+.om-block-handle {
position: fixed;
opacity: 1;
transition: opacity ease-in 0.2s;
@@ -282,12 +287,18 @@
pointer-events: none;
}
}
+.om-block-handle {
+ cursor: pointer;
+ background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KCjwhLS0gTGljZW5zZTogTUlULiBNYWRlIGJ5IGRldmVsb3BtZW50c2VlZDogaHR0cHM6Ly9naXRodWIuY29tL2RldmVsb3BtZW50c2VlZC9jb2xsZWN0aWNvbnMtbGliIC0tPgo8c3ZnIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDE2IDE2IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIGZpbGw9IiM3OTc5NzkiPgogIDxyZWN0IHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgaWQ9Imljb24tYm91bmQiIGZpbGw9Im5vbmUiIC8+CiAgPHBvbHlnb24gcG9pbnRzPSIxNSw3IDksNyA5LDEgNywxIDcsNyAxLDcgMSw5IDcsOSA3LDE1IDksMTUgOSw5IDE1LDkiIC8+Cjwvc3ZnPg==');
+}
.om-list-decimal {
list-style-type: decimal !important;
+ padding-left: 16px;
}
.om-list-disc {
list-style-type: disc !important;
+ padding-left: 16px;
}
.om-leading-normal {
line-height: 1.5;
diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json
index d567c5943e9..05de10ee103 100644
--- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json
+++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json
@@ -331,6 +331,7 @@
"domain-plural": "Bereiche",
"domain-type": "Bereichstyp",
"downstream-depth": "Nachgelagerte Tiefe",
+ "duplicate": "Duplicate",
"duration": "Dauer",
"edge": "Kante",
"edge-information": "Kanteninformationen",
diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json
index a667480afd9..349d7e6b903 100644
--- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json
+++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json
@@ -331,6 +331,7 @@
"domain-plural": "Domains",
"domain-type": "Domain Type",
"downstream-depth": "Downstream Depth",
+ "duplicate": "Duplicate",
"duration": "Duration",
"edge": "Edge",
"edge-information": "Edge Information",
diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json
index 5622a7b071e..64a2adb3f94 100644
--- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json
+++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json
@@ -331,6 +331,7 @@
"domain-plural": "Domains",
"domain-type": "Domain Type",
"downstream-depth": "Profundidad del flujo",
+ "duplicate": "Duplicate",
"duration": "Duration",
"edge": "Edge",
"edge-information": "Información de la arista",
diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json
index f1922d8b5df..bc05a0c1c90 100644
--- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json
+++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json
@@ -331,6 +331,7 @@
"domain-plural": "Domaines",
"domain-type": "Type de Domaine",
"downstream-depth": "Profondeur en Aval",
+ "duplicate": "Duplicate",
"duration": "Durée",
"edge": "Bord",
"edge-information": "Informations du Bord",
diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json
index fe1eada4e11..967870a53af 100644
--- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json
+++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json
@@ -331,6 +331,7 @@
"domain-plural": "Domains",
"domain-type": "Domain Type",
"downstream-depth": "Downstream Depth",
+ "duplicate": "Duplicate",
"duration": "Duration",
"edge": "Edge",
"edge-information": "エッジの情報",
diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json
index 48487453ddc..3b2c4ad1e3f 100644
--- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json
+++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json
@@ -331,6 +331,7 @@
"domain-plural": "Domains",
"domain-type": "Domain Type",
"downstream-depth": "Profundidade abaixo",
+ "duplicate": "Duplicate",
"duration": "Duration",
"edge": "Edge",
"edge-information": "Informação sobre o limite",
diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json
index 5f60cb95298..16ae2c8d9ca 100644
--- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json
+++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json
@@ -331,6 +331,7 @@
"domain-plural": "Domains",
"domain-type": "Domain Type",
"downstream-depth": "Нисходящая линия",
+ "duplicate": "Duplicate",
"duration": "Длительность",
"edge": "Edge",
"edge-information": "Граница информации",
diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json
index c7f44c3d045..0bc520e3ace 100644
--- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json
+++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json
@@ -331,6 +331,7 @@
"domain-plural": "域",
"domain-type": "域类型",
"downstream-depth": "下游深度",
+ "duplicate": "Duplicate",
"duration": "持续时间",
"edge": "Edge",
"edge-information": "连线信息",