feat(editor): Update sticky content when checkbox state changes (#9596)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Milorad FIlipović 2024-06-05 09:01:45 +02:00 committed by GitHub
parent 744c94d94b
commit 5361e9f69a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 137 additions and 4 deletions

View file

@ -5,6 +5,8 @@
ref="editor"
:class="$style[theme]"
@click="onClick"
@mousedown="onMouseDown"
@change="onChange"
v-html="htmlContent"
/>
<div v-else :class="$style.markdown">
@ -17,7 +19,7 @@
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, ref } from 'vue';
import type { Options as MarkdownOptions } from 'markdown-it';
import Markdown from 'markdown-it';
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 N8nLoading from '../N8nLoading';
import { escapeMarkdown } from '../../utils/markdown';
import { escapeMarkdown, toggleCheckbox } from '../../utils/markdown';
interface IImage {
id: string;
@ -79,6 +81,8 @@ const props = withDefaults(defineProps<MarkdownProps>(), {
}),
});
const editor = ref<HTMLDivElement | undefined>(undefined);
const { options } = props;
const md = new Markdown(options.markdown)
.use(markdownLink, options.linkAttributes)
@ -151,7 +155,7 @@ const htmlContent = computed(() => {
return safeHtml;
});
const $emit = defineEmits(['markdown-click']);
const $emit = defineEmits(['markdown-click', 'update-content']);
const onClick = (event: MouseEvent) => {
let clickedLink: HTMLAnchorElement | null = null;
@ -167,6 +171,40 @@ const onClick = (event: MouseEvent) => {
}
$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>
<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 {
color: var(--color-sticky-font);
@ -297,6 +343,11 @@ const onClick = (event: MouseEvent) => {
font-weight: var(--font-weight-regular);
line-height: var(--font-line-height-regular);
}
&:has(input[type='checkbox']) {
list-style-type: none;
padding-left: var(--spacing-5xs);
}
}
code {

View file

@ -1,4 +1,4 @@
import { render } from '@testing-library/vue';
import { render, fireEvent } from '@testing-library/vue';
import N8nMarkdown from '../Markdown.vue';
describe('components', () => {
@ -15,6 +15,7 @@ describe('components', () => {
expect(checkbox).not.toBeChecked();
});
});
it('should render checked checkboxes', () => {
const wrapper = render(N8nMarkdown, {
props: {
@ -27,6 +28,26 @@ describe('components', () => {
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', () => {
const wrapper = render(N8nMarkdown, {
props: {

View file

@ -27,6 +27,7 @@
:content="modelValue"
:with-multi-breaks="true"
@markdown-click="onMarkdownClick"
@update-content="onUpdateModelValue"
/>
</div>
<div

View 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);
});
});

View file

@ -11,3 +11,36 @@ export const escapeMarkdown = (html: string | undefined): string => {
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');
};