Skip to content

Commit 0e8c783

Browse files
committed
fix(texteditor): replace bubblemenu extensions to solve dependency conflict
1 parent a77e763 commit 0e8c783

6 files changed

Lines changed: 77 additions & 72 deletions

File tree

contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@
66
@insert-link="linkHandler.openLinkEditor()"
77
/>
88

9-
<!-- This div holds the bubble menu component and is passed to the editor -->
10-
<div ref="bubbleMenu">
9+
<div
10+
v-if="linkHandler.isBubbleMenuOpen.value"
11+
:style="linkHandler.popoverStyle.value"
12+
>
1113
<LinkBubbleMenu
1214
v-if="isReady"
1315
:editor="editor"
1416
/>
1517
</div>
1618

17-
<!-- The Popover for creating/editing links -->
1819
<div
1920
v-if="linkHandler.isEditorOpen.value"
20-
:style="linkHandler.editorStyle.value"
21+
:style="linkHandler.popoverStyle.value"
2122
>
2223
<LinkEditor
2324
:mode="linkHandler.editorMode.value"
@@ -56,7 +57,7 @@
5657

5758
<script>
5859
59-
import { defineComponent, provide, onMounted, ref } from 'vue';
60+
import { defineComponent, provide, onMounted } from 'vue';
6061
import EditorToolbar from './components/EditorToolbar.vue';
6162
import EditorContentWrapper from './components/EditorContentWrapper.vue';
6263
import { useEditor } from './composables/useEditor';
@@ -80,12 +81,12 @@
8081
const { editor, isReady, initializeEditor } = useEditor();
8182
provide('editor', editor);
8283
provide('isReady', isReady);
83-
const bubbleMenu = ref(null);
84+
85+
const linkHandler = useLinkHandling(editor);
86+
provide('linkHandler', linkHandler);
8487
8588
onMounted(() => {
86-
if (bubbleMenu.value) {
87-
initializeEditor(bubbleMenu.value, linkHandler.isEditorOpen);
88-
}
89+
initializeEditor();
8990
});
9091
9192
const {
@@ -100,11 +101,8 @@
100101
handleRemove,
101102
} = useImageHandling(editor);
102103
103-
const linkHandler = useLinkHandling(editor);
104-
provide('linkHandler', linkHandler);
105-
106104
const handleDrop = event => {
107-
const file = event.dataTransfer?.files[0];
105+
const file = event.dataTransfer.files[0];
108106
if (file) {
109107
openCreateModal(file);
110108
}
@@ -124,7 +122,6 @@
124122
isModalCentered,
125123
linkHandler,
126124
editor,
127-
bubbleMenu,
128125
};
129126
},
130127
});

contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import StarterKitExtension from '@tiptap/starter-kit';
44
import UnderlineExtension from '@tiptap/extension-underline';
55
import { Superscript } from '@tiptap/extension-superscript';
66
import { Subscript } from '@tiptap/extension-subscript';
7-
import { BubbleMenu } from '@tiptap/extension-bubble-menu';
87
import { Small } from '../extensions/SmallTextExtension';
98
import { Image } from '../extensions/Image';
109
import { CodeBlockSyntaxHighlight } from '../extensions/CodeBlockSyntaxHighlight';
@@ -14,8 +13,7 @@ export function useEditor() {
1413
const editor = ref(null);
1514
const isReady = ref(false);
1615

17-
// Accept the bubble menu element as an argument
18-
const initializeEditor = (bubbleMenuElement, isLinkEditorOpen) => {
16+
const initializeEditor = () => {
1917
editor.value = new Editor({
2018
extensions: [
2119
StarterKitExtension.configure({
@@ -29,17 +27,6 @@ export function useEditor() {
2927
Subscript,
3028
Image,
3129
CustomLink, // Use our custom Link extension
32-
BubbleMenu.configure({
33-
// Use the passed-in element directly
34-
element: bubbleMenuElement,
35-
tippyOptions: {
36-
placement: 'bottom-start',
37-
inertia: true,
38-
},
39-
shouldShow: ({ editor }) => {
40-
return !isLinkEditorOpen.value && editor.isActive('link');
41-
},
42-
}),
4330
],
4431
content: '<p></p>',
4532
editorProps: {

contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useLinkHandling.js

Lines changed: 65 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { ref, watch, onMounted, onUnmounted } from 'vue';
1+
import { ref, watch, onUnmounted } from 'vue';
22

33
export function useLinkHandling(editor) {
44
const isEditorOpen = ref(false);
5-
const editorStyle = ref({});
5+
const isBubbleMenuOpen = ref(false);
6+
const popoverStyle = ref({});
67
const editorInitialState = ref({ text: '', href: '' });
78
const editorMode = ref('create');
89
const savedSelection = ref(null);
@@ -23,33 +24,28 @@ export function useLinkHandling(editor) {
2324
const openLinkEditor = (mode = 'create') => {
2425
if (!editor.value) return;
2526

27+
closeBubbleMenu(); // Ensure bubble menu is closed
28+
2629
const { state } = editor.value;
2730
const { from, to, empty } = state.selection;
2831

29-
editorStyle.value = calculatePosition();
32+
popoverStyle.value = calculatePosition();
3033
editorMode.value = mode;
3134

3235
if (mode === 'edit') {
33-
// For editing, use TipTap's extendMarkRange to get the full link range
3436
const linkAttrs = editor.value.getAttributes('link');
3537
if (!linkAttrs.href) {
3638
closeLinkEditor();
3739
return;
3840
}
39-
40-
// Execute the extendMarkRange command to actually extend the selection
4141
editor.value.chain().focus().extendMarkRange('link').run();
42-
43-
// Get the new selection after extending
4442
const newSelection = editor.value.state.selection;
45-
4643
savedSelection.value = { from: newSelection.from, to: newSelection.to };
4744
editorInitialState.value = {
4845
href: linkAttrs.href,
4946
text: state.doc.textBetween(newSelection.from, newSelection.to, ' '),
5047
};
5148
} else {
52-
// For creating, use current selection
5349
savedSelection.value = { from, to };
5450
editorInitialState.value = {
5551
href: '',
@@ -63,69 +59,105 @@ export function useLinkHandling(editor) {
6359
const closeLinkEditor = () => {
6460
isEditorOpen.value = false;
6561
savedSelection.value = null;
66-
editorMode.value = 'create'; // Reset to default
62+
editorMode.value = 'create';
63+
};
64+
65+
const openBubbleMenu = () => {
66+
if (isEditorOpen.value) return;
67+
popoverStyle.value = calculatePosition();
68+
isBubbleMenuOpen.value = true;
69+
};
70+
71+
const closeBubbleMenu = () => {
72+
isBubbleMenuOpen.value = false;
73+
};
74+
75+
const closeAll = () => {
76+
closeLinkEditor();
77+
closeBubbleMenu();
6778
};
6879

6980
const saveLink = ({ text, href }) => {
7081
if (!editor.value || !savedSelection.value) {
71-
return closeLinkEditor();
82+
return closeAll();
7283
}
7384

7485
const { from, to } = savedSelection.value;
75-
7686
const tr = editor.value.state.tr;
7787
const linkMark = editor.value.state.schema.marks.link.create({ href });
78-
7988
tr.replaceWith(from, to, editor.value.state.schema.text(text, [linkMark]));
8089
editor.value.view.dispatch(tr);
8190

82-
closeLinkEditor();
91+
closeAll();
8392
};
8493

8594
const removeLink = () => {
8695
editor.value?.chain().focus().extendMarkRange('link').unsetLink().run();
87-
closeLinkEditor();
96+
closeAll();
8897
};
8998

90-
watch(isEditorOpen, isOpen => {
91-
const handler = event => {
92-
const popover = document.querySelector('.link-editor-popover');
99+
const handleSelectionUpdate = () => {
100+
if (editor.value.isActive('link')) {
101+
openBubbleMenu();
102+
} else {
103+
closeBubbleMenu();
104+
}
105+
};
106+
107+
watch([isEditorOpen, isBubbleMenuOpen], ([editorIsOpen, bubbleIsOpen]) => {
108+
const isOpen = editorIsOpen || bubbleIsOpen;
109+
const clickHandler = event => {
110+
const popover =
111+
document.querySelector('.link-editor-popover') ||
112+
document.querySelector('.link-bubble-menu');
93113
if (popover && !popover.contains(event.target)) {
94-
closeLinkEditor();
114+
closeAll();
95115
}
96116
};
117+
const scrollHandler = () => closeAll();
118+
97119
if (isOpen) {
98120
setTimeout(() => {
99-
document.addEventListener('mousedown', handler, true);
121+
document.addEventListener('mousedown', clickHandler, true);
122+
document.addEventListener('scroll', scrollHandler, true);
100123
}, 0);
101124
} else {
102-
document.removeEventListener('mousedown', handler, true);
125+
document.removeEventListener('mousedown', clickHandler, true);
126+
document.removeEventListener('scroll', scrollHandler, true);
103127
}
104128
});
105129

106-
const handleOpenLinkEditorEvent = () => {
107-
openLinkEditor();
108-
};
109-
110-
onMounted(() => {
111-
if (editor?.value) {
112-
editor.value.on('open-link-editor', handleOpenLinkEditorEvent);
113-
}
114-
});
130+
watch(
131+
() => editor.value,
132+
(newEditor, oldEditor) => {
133+
if (oldEditor) {
134+
oldEditor.off('open-link-editor', openLinkEditor);
135+
oldEditor.off('selectionUpdate', handleSelectionUpdate);
136+
}
137+
if (newEditor) {
138+
newEditor.on('open-link-editor', openLinkEditor);
139+
newEditor.on('selectionUpdate', handleSelectionUpdate);
140+
}
141+
},
142+
{ immediate: true },
143+
);
115144

116145
onUnmounted(() => {
117146
if (editor?.value) {
118-
editor.value.off('open-link-editor', handleOpenLinkEditorEvent);
147+
editor.value.off('open-link-editor', openLinkEditor);
148+
editor.value.off('selectionUpdate', handleSelectionUpdate);
119149
}
120150
});
121151

122152
return {
123153
isEditorOpen,
124-
editorStyle,
154+
isBubbleMenuOpen,
155+
popoverStyle,
125156
editorInitialState,
126157
editorMode,
127158
openLinkEditor,
128159
closeLinkEditor,
160+
closeBubbleMenu,
129161
saveLink,
130162
removeLink,
131163
};

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
"dependencies": {
5656
"@sentry/vue": "^7.112.2",
5757
"@tiptap/core": "^2.14.0",
58-
"@tiptap/extension-bubble-menu": "^2.23.1",
5958
"@tiptap/extension-code-block-lowlight": "^2.23.0",
6059
"@tiptap/extension-link": "^2.23.1",
6160
"@tiptap/extension-subscript": "^2.14.0",

pnpm-lock.yaml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webpack.config.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ const baseConfig = require('kolibri-tools/lib/webpack.config.base');
99
const { merge } = require('webpack-merge');
1010
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
1111

12-
const tippyPath = path.dirname(require.resolve('tippy.js/package.json', { paths: [require.resolve('@tiptap/core')] }));
13-
1412
const BundleTracker = require('kolibri-tools/lib/webpackBundleTracker');
1513
const CircularDependencyPlugin = require('circular-dependency-plugin');
1614

@@ -150,11 +148,6 @@ module.exports = (env = {}) => {
150148
// needed to reference Vuetify styles in the shadow DOM
151149
vuetify: path.resolve('node_modules', 'vuetify'),
152150
static: staticFilesDir,
153-
// Temp fix
154-
// Added this alias to resolve the tippy.js conflict.
155-
// It redirects the old, invalid import path from kolibri-design-system
156-
// to the correct file in the newer tippy.js version required by Tiptap.
157-
'tippy.js/umd': path.join(tippyPath, 'dist/tippy.umd.js'),
158151
},
159152
extensions: ['.js', '.vue', '.css'],
160153
symlinks: true,

0 commit comments

Comments
 (0)