mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-26 20:02:26 -08:00
feat(editor): Update sticky content when checkbox state changes (#9596)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
parent
744c94d94b
commit
5361e9f69a
|
@ -5,6 +5,8 @@
|
||||||
ref="editor"
|
ref="editor"
|
||||||
:class="$style[theme]"
|
:class="$style[theme]"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
|
@mousedown="onMouseDown"
|
||||||
|
@change="onChange"
|
||||||
v-html="htmlContent"
|
v-html="htmlContent"
|
||||||
/>
|
/>
|
||||||
<div v-else :class="$style.markdown">
|
<div v-else :class="$style.markdown">
|
||||||
|
@ -17,7 +19,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import type { Options as MarkdownOptions } from 'markdown-it';
|
import type { Options as MarkdownOptions } from 'markdown-it';
|
||||||
import Markdown from 'markdown-it';
|
import Markdown from 'markdown-it';
|
||||||
import markdownLink from 'markdown-it-link-attributes';
|
import markdownLink from 'markdown-it-link-attributes';
|
||||||
|
@ -26,7 +28,7 @@ import markdownTaskLists from 'markdown-it-task-lists';
|
||||||
import xss, { friendlyAttrValue, whiteList } from 'xss';
|
import xss, { friendlyAttrValue, whiteList } from 'xss';
|
||||||
|
|
||||||
import N8nLoading from '../N8nLoading';
|
import N8nLoading from '../N8nLoading';
|
||||||
import { escapeMarkdown } from '../../utils/markdown';
|
import { escapeMarkdown, toggleCheckbox } from '../../utils/markdown';
|
||||||
|
|
||||||
interface IImage {
|
interface IImage {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -79,6 +81,8 @@ const props = withDefaults(defineProps<MarkdownProps>(), {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const editor = ref<HTMLDivElement | undefined>(undefined);
|
||||||
|
|
||||||
const { options } = props;
|
const { options } = props;
|
||||||
const md = new Markdown(options.markdown)
|
const md = new Markdown(options.markdown)
|
||||||
.use(markdownLink, options.linkAttributes)
|
.use(markdownLink, options.linkAttributes)
|
||||||
|
@ -151,7 +155,7 @@ const htmlContent = computed(() => {
|
||||||
return safeHtml;
|
return safeHtml;
|
||||||
});
|
});
|
||||||
|
|
||||||
const $emit = defineEmits(['markdown-click']);
|
const $emit = defineEmits(['markdown-click', 'update-content']);
|
||||||
const onClick = (event: MouseEvent) => {
|
const onClick = (event: MouseEvent) => {
|
||||||
let clickedLink: HTMLAnchorElement | null = null;
|
let clickedLink: HTMLAnchorElement | null = null;
|
||||||
|
|
||||||
|
@ -167,6 +171,40 @@ const onClick = (event: MouseEvent) => {
|
||||||
}
|
}
|
||||||
$emit('markdown-click', clickedLink, event);
|
$emit('markdown-click', clickedLink, event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle checkbox changes
|
||||||
|
const onChange = async (event: Event) => {
|
||||||
|
if (event.target instanceof HTMLInputElement && event.target.type === 'checkbox') {
|
||||||
|
const checkboxes = editor.value?.querySelectorAll('input[type="checkbox"]');
|
||||||
|
if (checkboxes) {
|
||||||
|
// Get the index of the checkbox that was clicked
|
||||||
|
const index = Array.from(checkboxes).indexOf(event.target);
|
||||||
|
if (index !== -1) {
|
||||||
|
onCheckboxChange(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseDown = (event: MouseEvent) => {
|
||||||
|
// Mouse down on input fields is caught by node view handlers
|
||||||
|
// which prevents checking them, this will prevent that
|
||||||
|
if (event.target instanceof HTMLInputElement) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update markdown when checkbox state changes
|
||||||
|
const onCheckboxChange = (index: number) => {
|
||||||
|
const currentContent = props.content;
|
||||||
|
if (!currentContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are using index to connect the checkbox with the corresponding line in the markdown
|
||||||
|
const newContent = toggleCheckbox(currentContent, index);
|
||||||
|
$emit('update-content', newContent);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -243,6 +281,14 @@ const onClick = (event: MouseEvent) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
accent-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] + label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.sticky {
|
.sticky {
|
||||||
color: var(--color-sticky-font);
|
color: var(--color-sticky-font);
|
||||||
|
|
||||||
|
@ -297,6 +343,11 @@ const onClick = (event: MouseEvent) => {
|
||||||
font-weight: var(--font-weight-regular);
|
font-weight: var(--font-weight-regular);
|
||||||
line-height: var(--font-line-height-regular);
|
line-height: var(--font-line-height-regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:has(input[type='checkbox']) {
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: var(--spacing-5xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { render } from '@testing-library/vue';
|
import { render, fireEvent } from '@testing-library/vue';
|
||||||
import N8nMarkdown from '../Markdown.vue';
|
import N8nMarkdown from '../Markdown.vue';
|
||||||
|
|
||||||
describe('components', () => {
|
describe('components', () => {
|
||||||
|
@ -15,6 +15,7 @@ describe('components', () => {
|
||||||
expect(checkbox).not.toBeChecked();
|
expect(checkbox).not.toBeChecked();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render checked checkboxes', () => {
|
it('should render checked checkboxes', () => {
|
||||||
const wrapper = render(N8nMarkdown, {
|
const wrapper = render(N8nMarkdown, {
|
||||||
props: {
|
props: {
|
||||||
|
@ -27,6 +28,26 @@ describe('components', () => {
|
||||||
expect(checkbox).toBeChecked();
|
expect(checkbox).toBeChecked();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should toggle checkboxes when clicked', async () => {
|
||||||
|
const wrapper = render(N8nMarkdown, {
|
||||||
|
props: {
|
||||||
|
content: '__TODO__\n- [ ] Buy milk\n- [ ] Buy socks\n',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const checkboxes = wrapper.getAllByRole('checkbox');
|
||||||
|
expect(checkboxes).toHaveLength(2);
|
||||||
|
expect(checkboxes[0]).not.toBeChecked();
|
||||||
|
expect(checkboxes[1]).not.toBeChecked();
|
||||||
|
|
||||||
|
await fireEvent.click(checkboxes[0]);
|
||||||
|
expect(checkboxes[0]).toBeChecked();
|
||||||
|
expect(checkboxes[1]).not.toBeChecked();
|
||||||
|
|
||||||
|
const updatedContent = wrapper.emitted()['update-content'][0];
|
||||||
|
expect(updatedContent).toEqual(['__TODO__\n- [x] Buy milk\n- [ ] Buy socks\n']);
|
||||||
|
});
|
||||||
|
|
||||||
it('should render inputs as plain text', () => {
|
it('should render inputs as plain text', () => {
|
||||||
const wrapper = render(N8nMarkdown, {
|
const wrapper = render(N8nMarkdown, {
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
:content="modelValue"
|
:content="modelValue"
|
||||||
:with-multi-breaks="true"
|
:with-multi-breaks="true"
|
||||||
@markdown-click="onMarkdownClick"
|
@markdown-click="onMarkdownClick"
|
||||||
|
@update-content="onUpdateModelValue"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
27
packages/design-system/src/utils/__tests__/markdown.spec.ts
Normal file
27
packages/design-system/src/utils/__tests__/markdown.spec.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { toggleCheckbox } from '../markdown';
|
||||||
|
|
||||||
|
describe('toggleCheckbox', () => {
|
||||||
|
it('should do nothing when there are no checkboxes', () => {
|
||||||
|
const content = '"## I\'m a note \n Test\n"';
|
||||||
|
expect(toggleCheckbox(content, 0)).toBe(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle a checkbox at a specific index', () => {
|
||||||
|
const content = '"## I\'m a note \n* [ ] First\n* [ ] Second\n* [ ] Third\n"';
|
||||||
|
expect(toggleCheckbox(content, 0)).toBe(
|
||||||
|
'"## I\'m a note \n* [x] First\n* [ ] Second\n* [ ] Third\n"',
|
||||||
|
);
|
||||||
|
expect(toggleCheckbox(content, 1)).toBe(
|
||||||
|
'"## I\'m a note \n* [ ] First\n* [x] Second\n* [ ] Third\n"',
|
||||||
|
);
|
||||||
|
expect(toggleCheckbox(content, 2)).toBe(
|
||||||
|
'"## I\'m a note \n* [ ] First\n* [ ] Second\n* [x] Third\n"',
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedContent = toggleCheckbox(content, 1);
|
||||||
|
expect(toggleCheckbox(updatedContent, 0)).toBe(
|
||||||
|
'"## I\'m a note \n* [x] First\n* [x] Second\n* [ ] Third\n"',
|
||||||
|
);
|
||||||
|
expect(toggleCheckbox(updatedContent, 1)).toBe(content);
|
||||||
|
});
|
||||||
|
});
|
|
@ -11,3 +11,36 @@ export const escapeMarkdown = (html: string | undefined): string => {
|
||||||
|
|
||||||
return withQuotes;
|
return withQuotes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkedRegEx = /(\*|-) \[x\]/;
|
||||||
|
const uncheckedRegEx = /(\*|-) \[\s\]/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the checkbox at the specified index in the given markdown string.
|
||||||
|
*
|
||||||
|
* @param markdown - The markdown string containing checkboxes.
|
||||||
|
* @param index - The index of the checkbox to toggle.
|
||||||
|
* @returns The updated markdown string with the checkbox toggled.
|
||||||
|
*/
|
||||||
|
export const toggleCheckbox = (markdown: string, index: number) => {
|
||||||
|
let cursor = 0;
|
||||||
|
const lines = markdown.split('\n');
|
||||||
|
|
||||||
|
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
|
||||||
|
const line = lines[lineNumber];
|
||||||
|
const checked = checkedRegEx.test(line);
|
||||||
|
const unchecked = uncheckedRegEx.test(line);
|
||||||
|
|
||||||
|
if (checked || unchecked) {
|
||||||
|
if (cursor === index) {
|
||||||
|
const regExp = checked ? checkedRegEx : uncheckedRegEx;
|
||||||
|
const replacement = checked ? '[ ]' : '[x]';
|
||||||
|
lines[lineNumber] = line.replace(regExp, `$1 ${replacement}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cursor++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue