mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix: Empty onclick breaks range parser in HTML editor (#18032)
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
|||||||
import { Prec, EditorState } from '@codemirror/state';
|
import { Prec, EditorState } from '@codemirror/state';
|
||||||
import {
|
import {
|
||||||
dropCursor,
|
dropCursor,
|
||||||
|
EditorView,
|
||||||
highlightActiveLine,
|
highlightActiveLine,
|
||||||
highlightActiveLineGutter,
|
highlightActiveLineGutter,
|
||||||
keymap,
|
keymap,
|
||||||
@@ -35,7 +36,7 @@ import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
|
|||||||
import { autoCloseTags, htmlLanguage } from 'codemirror-lang-html-n8n';
|
import { autoCloseTags, htmlLanguage } from 'codemirror-lang-html-n8n';
|
||||||
import { codeEditorTheme } from '../CodeNodeEditor/theme';
|
import { codeEditorTheme } from '../CodeNodeEditor/theme';
|
||||||
import type { Range, Section } from './types';
|
import type { Range, Section } from './types';
|
||||||
import { nonTakenRanges } from './utils';
|
import { nonTakenRanges, pasteHandler } from './utils';
|
||||||
import type { TargetNodeParameterContext } from '@/Interface';
|
import type { TargetNodeParameterContext } from '@/Interface';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -67,6 +68,7 @@ const extensions = computed(() => [
|
|||||||
]),
|
]),
|
||||||
autoCloseTags,
|
autoCloseTags,
|
||||||
expressionCloseBrackets(),
|
expressionCloseBrackets(),
|
||||||
|
pasteSanitizer(),
|
||||||
Prec.highest(keymap.of(editorKeymap)),
|
Prec.highest(keymap.of(editorKeymap)),
|
||||||
indentOnInput(),
|
indentOnInput(),
|
||||||
codeEditorTheme({
|
codeEditorTheme({
|
||||||
@@ -231,6 +233,12 @@ async function formatHtml() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pasteSanitizer() {
|
||||||
|
return EditorView.domEventHandlers({
|
||||||
|
paste: pasteHandler,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
htmlEditorEventBus.on('format-html', formatHtml);
|
htmlEditorEventBus.on('format-html', formatHtml);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest'; // Change to jest if needed
|
||||||
|
import { pasteHandler } from './utils';
|
||||||
|
import type { EditorView } from '@codemirror/view';
|
||||||
|
|
||||||
|
describe('pasteHandler', () => {
|
||||||
|
it('replaces empty onclick attributes and dispatches changes', () => {
|
||||||
|
const clipboardData = {
|
||||||
|
getData: (type: string) => (type === 'text/html' ? '<div onclick=""></div>' : ''),
|
||||||
|
};
|
||||||
|
|
||||||
|
const preventDefault = vi.fn();
|
||||||
|
const dispatch = vi.fn();
|
||||||
|
|
||||||
|
const event = { clipboardData, preventDefault } as unknown as ClipboardEvent;
|
||||||
|
const view = {
|
||||||
|
state: {
|
||||||
|
selection: {
|
||||||
|
main: { from: 0, to: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dispatch,
|
||||||
|
} as unknown as EditorView;
|
||||||
|
|
||||||
|
const result = pasteHandler(event, view);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(preventDefault).toHaveBeenCalled();
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
insert: '<div onclick=" "></div>',
|
||||||
|
},
|
||||||
|
scrollIntoView: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if there is no onclick="" in pasted content', () => {
|
||||||
|
const clipboardData = {
|
||||||
|
getData: (type: string) => (type === 'text/html' ? '<p>Hello world</p>' : ''),
|
||||||
|
};
|
||||||
|
const preventDefault = vi.fn();
|
||||||
|
const dispatch = vi.fn();
|
||||||
|
|
||||||
|
const event = { clipboardData, preventDefault } as unknown as ClipboardEvent;
|
||||||
|
const view = {
|
||||||
|
state: {
|
||||||
|
selection: {
|
||||||
|
main: { from: 0, to: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dispatch,
|
||||||
|
} as unknown as EditorView;
|
||||||
|
|
||||||
|
const result = pasteHandler(event, view);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(preventDefault).not.toHaveBeenCalled();
|
||||||
|
expect(dispatch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if clipboard data is empty', () => {
|
||||||
|
const clipboardData = {
|
||||||
|
getData: () => '',
|
||||||
|
};
|
||||||
|
const preventDefault = vi.fn();
|
||||||
|
const dispatch = vi.fn();
|
||||||
|
|
||||||
|
const event = { clipboardData, preventDefault } as unknown as ClipboardEvent;
|
||||||
|
const view = {
|
||||||
|
state: {
|
||||||
|
selection: {
|
||||||
|
main: { from: 0, to: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dispatch,
|
||||||
|
} as unknown as EditorView;
|
||||||
|
|
||||||
|
const result = pasteHandler(event, view);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(preventDefault).not.toHaveBeenCalled();
|
||||||
|
expect(dispatch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { Range } from './types';
|
import type { Range } from './types';
|
||||||
|
|
||||||
|
import type { EditorView } from '@codemirror/view';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the ranges of a full range that are _not_ within the taken ranges,
|
* Return the ranges of a full range that are _not_ within the taken ranges,
|
||||||
* assuming sorted taken ranges. e.g. `[0, 10]` and `[[2, 3], [7, 8]]`
|
* assuming sorted taken ranges. e.g. `[0, 10]` and `[[2, 3], [7, 8]]`
|
||||||
@@ -38,3 +40,25 @@ export function nonTakenRanges(fullRange: Range, takenRanges: Range[]) {
|
|||||||
|
|
||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pasteHandler(event: ClipboardEvent, view: EditorView) {
|
||||||
|
const htmlData = event.clipboardData?.getData('text/html') ?? '';
|
||||||
|
const textData = event.clipboardData?.getData('text/plain') ?? '';
|
||||||
|
|
||||||
|
const content = htmlData || textData;
|
||||||
|
if (!content) return false;
|
||||||
|
|
||||||
|
// Replace onclick="" with onclick=" " as empty onclick breaks range parser
|
||||||
|
if (/onclick=""/.test(content)) {
|
||||||
|
event.preventDefault();
|
||||||
|
const sanitized = content.replace(/onclick=""/g, 'onclick=" "');
|
||||||
|
const { from, to } = view.state.selection.main;
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from, to, insert: sanitized },
|
||||||
|
scrollIntoView: true,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user