Skip to content

Commit

Permalink
chore: maybe fix memory leak
Browse files Browse the repository at this point in the history
surely not, the leak is inside note formatter, yet to be discovered
maybe...

chore: don't forget to destroy editor
  • Loading branch information
logotip4ik committed Oct 25, 2024
1 parent 3c600f7 commit 25981ab
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 46 deletions.
34 changes: 18 additions & 16 deletions components/Workspace/Note/Editor/NoteEditor.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { Editor } from '@tiptap/vue-3';
import {
LazyWorkspaceNoteFormatterBubbleBox as LazyBubbleBox,
LazyWorkspaceNoteFormatterFixedBox as LazyFixedBox,
Expand Down Expand Up @@ -29,7 +31,7 @@ const {
let hasUnsavedChanges = false;
function updateContent(force?: boolean) {
const content = editor.value?.getHTML();
const content = editor.getHTML();
return props
.onUpdate(content || '', force)
Expand All @@ -39,29 +41,29 @@ function updateContent(force?: boolean) {
}
watch(() => props.note.content, (content) => {
if (isTyping.value || !editor.value) {
if (isTyping.value || !editor) {
return;
}
const editorContent = editor.value.getHTML();
const editorContent = editor.getHTML();
if (editorContent !== content) {
editor.value.commands.setContent(content || '');
editor.commands.setContent(content || '');
}
}, {
immediate: import.meta.client,
deep: true, // this is really weird, but it only triggers watcher with deep: true ?
});
watch(() => props.editable, (editable) => {
editor.value?.setOptions({ editable });
editor.setOptions({ editable });
}, { immediate: import.meta.client });
watch(spellcheck, (spellcheck) => {
editor.value?.setOptions({
editor.setOptions({
editorProps: {
attributes: {
...editor.value.options.editorProps.attributes,
...editor.options.editorProps.attributes,
spellcheck: spellcheck === 'yes' ? 'true' : 'false',
},
},
Expand All @@ -71,7 +73,7 @@ watch(spellcheck, (spellcheck) => {
mitt.on('save:note', () => updateContent(true));
zeenk.on('update-note', ({ path, steps }) => {
if (props.note.path !== path || !editor.value) {
if (props.note.path !== path || !editor) {
return;
}
Expand All @@ -80,16 +82,16 @@ zeenk.on('update-note', ({ path, steps }) => {
return;
}
const tr = editor.value.state.tr;
const tr = editor.state.tr;
tr.setMeta('websocket', true);
for (const step of steps) {
tr.step(
Step.fromJSON(editor.value.state.schema, step),
Step.fromJSON(editor.state.schema, step),
);
}
editor.value.view.dispatch(tr);
editor.view.dispatch(tr);
});
function saveUnsavedChanges() {
Expand Down Expand Up @@ -124,18 +126,18 @@ useTinykeys({
[shortcuts.edit]: (event) => {
const target = event.target as HTMLElement;
if (target.tagName !== 'INPUT' && target !== editor.value?.view.dom) {
if (target.tagName !== 'INPUT' && target !== editor.view.dom) {
event.preventDefault();
editor.value?.commands.focus();
editor.commands.focus();
}
},
'Escape': () => {
editor.value?.commands.blur();
editor.commands.blur();
},
'$mod+s': (event) => {
if (!editor.value?.isFocused) {
if (!editor.isFocused) {
return;
}
Expand Down Expand Up @@ -176,6 +178,6 @@ if (import.meta.client) {
<WorkspaceNoteFormatter :editor />
</Component>

<EditorContent class="note-editor" :editor />
<EditorContent class="note-editor" :editor="editor as Editor" />
</div>
</template>
36 changes: 27 additions & 9 deletions composables/tiptap/extensions/bubble-menu/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ComputePositionConfig } from '@floating-ui/dom';
import type { Editor } from '@tiptap/core';
import type { Editor, EditorEvents } from '@tiptap/core';

import type { EditorState } from '@tiptap/pm/state';
import type { EditorView } from '@tiptap/pm/view';
Expand All @@ -22,6 +22,15 @@ export type BubbleMenuViewProps = BubbleMenuPluginProps & {
view: EditorView
};

function editorOn<T extends keyof EditorEvents>(
editor: Editor,
event: T,
cb: (...props: EditorEvents[T] extends Array<any> ? EditorEvents[T] : [EditorEvents[T]]) => void,
): () => void {
editor.on(event, cb);
return () => editor.off(event, cb);
}

export class BubbleMenuView {
private editor: Editor;

Expand All @@ -39,6 +48,8 @@ export class BubbleMenuView {

private updateDebounceTimer: number | undefined;

private offs: Array<() => void>;

constructor({
editor,
element,
Expand All @@ -50,10 +61,12 @@ export class BubbleMenuView {
this.view = view;
this.updateDelay = 75;

this.element.addEventListener('mousedown', this.mousedownHandler.bind(this), { capture: true });
this.view.dom.addEventListener('dragstart', this.dragstartHandler.bind(this));
this.editor.on('focus', this.focusHandler.bind(this));
this.editor.on('blur', this.blurHandler.bind(this));
this.offs = [
on(this.element, 'mousedown', () => this.mousedownHandler(), { capture: true }),
on(this.view.dom, 'dragstart', () => this.dragstartHandler()),
editorOn(this.editor, 'focus', () => this.focusHandler()),
editorOn(this.editor, 'blur', (payload) => this.blurHandler(payload)),
];

this.hide();

Expand Down Expand Up @@ -253,10 +266,15 @@ export class BubbleMenuView {
}

destroy() {
this.element.removeEventListener('mousedown', this.mousedownHandler, { capture: true });
this.view.dom.removeEventListener('dragstart', this.dragstartHandler);
this.editor.off('focus', this.focusHandler);
this.editor.off('blur', this.blurHandler);
invokeArrayFns(this.offs);
this.offs.length = 0;

// @ts-expect-error cleaning up memory
this.editor = undefined;
// @ts-expect-error cleaning up memory
this.element = undefined;
// @ts-expect-error cleaning up memory
this.view = undefined;
}
}

Expand Down
6 changes: 3 additions & 3 deletions composables/tiptap/extensions/emoji-picker/EmojiPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ const props = defineProps<{
getBoundingClientRect?: () => DOMRect
}>();
const { editor } = useTiptap();
const emojiPickerEl = shallowRef<HTMLDivElement | null>(null);
const selectedEmoji = ref(0);
Expand Down Expand Up @@ -60,7 +58,9 @@ function handleKeypress(event: KeyboardEvent) {
event.preventDefault();
}
else if (event.key === 'ArrowUp') {
editor.value?.commands.focus();
withTiptapEditor((editor) => {
editor.commands.focus();
});
}
else if (
(event.key === 'ArrowLeft' || event.key === 'ArrowRight')
Expand Down
48 changes: 30 additions & 18 deletions composables/tiptap/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Editor as CoreEditor } from '@tiptap/core';

import type { Transaction } from '@tiptap/pm/state';

import { Editor } from '@tiptap/core';

import Blockquote from '@tiptap/extension-blockquote';
import Bold from '@tiptap/extension-bold';
import BulletList from '@tiptap/extension-bullet-list';
Expand All @@ -19,15 +20,12 @@ import Placeholder from '@tiptap/extension-placeholder';
import Strike from '@tiptap/extension-strike';
import TaskItem from '@tiptap/extension-task-item';
import TaskList from '@tiptap/extension-task-list';

import Text from '@tiptap/extension-text';
import { Editor } from '@tiptap/vue-3';

import { BubbleMenu } from './extensions/bubble-menu';
import { EmojiPicker } from './extensions/emoji-picker';
import { Link } from './extensions/link';

const editor = /* #__PURE__ */ shallowRef<Editor>();
const isTyping = /* #__PURE__ */ ref(false);

const debouncedClearTyping = debounce(() => isTyping.value = false, 500);
Expand Down Expand Up @@ -95,29 +93,43 @@ function initTiptap() {
});
}

const currentTiptap = shallowRef<Editor>();
export function useTiptap() {
if (!editor.value) {
editor.value = initTiptap();
}
const editor = initTiptap()!;

currentTiptap.value = editor;

const offs: Array<() => void> = [
() => currentTiptap.value = undefined,
];

return { editor, isTyping, onUpdate };
onScopeDispose(() => {
invokeArrayFns(offs);
offs.length = 0;
editor.destroy();
});

return {
editor,
isTyping,
onUpdate: (cb: (e: { editor: Editor, transaction: Transaction }) => void) => {
editor.on('update', cb);

offs.push(() => editor.off('update', cb));
},
};
}

export function withTiptapEditor(cb: (editor: Editor) => void) {
if (editor.value) {
return cb(editor.value);
const tiptap = currentTiptap.value;
if (tiptap) {
return cb(tiptap);
}

const stop = watch(editor, (editor) => {
const stop = watch(currentTiptap, (editor) => {
if (editor) {
stop();
cb(editor);
}
});
}

function onUpdate(cb: (e: { editor: CoreEditor, transaction: Transaction }) => void) {
withTiptapEditor((editor) => editor.on('update', cb));

onScopeDispose(() => editor.value?.off('update', cb));
}

0 comments on commit 25981ab

Please sign in to comment.