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": "连线信息",